You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@kylin.apache.org by xx...@apache.org on 2022/01/24 07:20:17 UTC

[kylin] branch kylin4_on_cloud updated: # init && deploy kylin4 on aws (#1801)

This is an automated email from the ASF dual-hosted git repository.

xxyu pushed a commit to branch kylin4_on_cloud
in repository https://gitbox.apache.org/repos/asf/kylin.git


The following commit(s) were added to refs/heads/kylin4_on_cloud by this push:
     new 08ef365  # init && deploy kylin4 on aws (#1801)
08ef365 is described below

commit 08ef36539cf6285ba1724f37dfcbf070a77b373e
Author: Tengting Xu <34...@users.noreply.github.com>
AuthorDate: Mon Jan 24 15:20:08 2022 +0800

    # init && deploy kylin4 on aws (#1801)
    
    * # init && deploy kylin4 on aws
    
    * # add license header
---
 .gitignore                                         |    9 +
 README.md                                          |  574 ++++++
 __init__.py                                        |   16 +
 backup/properties/default/kylin.properties         |   67 +
 .../properties/templates/kylin.properties.template |   65 +
 backup/scripts/prepare-ec2-env-for-kylin4.sh       |  755 +++++++
 backup/scripts/prepare-ec2-env-for-spark-master.sh |  587 ++++++
 backup/scripts/prepare-ec2-env-for-spark-slave.sh  |  477 +++++
 .../scripts/prepare-ec2-env-for-static-services.sh |  622 ++++++
 backup/scripts/prepare-ec2-env-for-zk.sh           |  336 +++
 bin/init.sh                                        |   81 +
 client/__init__.py                                 |   16 +
 client/basic.py                                    |  133 ++
 client/lightning.py                                |  116 ++
 .../ec2-cluster-kylin4-template.yaml               |  252 +++
 cloudformation_templates/ec2-cluster-kylin4.yaml   |  251 +++
 cloudformation_templates/ec2-cluster-rds.yaml      |  149 ++
 .../ec2-cluster-spark-master.yaml                  |  219 ++
 .../ec2-cluster-spark-slave-template.yaml          |  210 ++
 .../ec2-cluster-spark-slave.yaml                   |  377 ++++
 .../ec2-cluster-static-services.yaml               |  211 ++
 cloudformation_templates/ec2-cluster-zk.yaml       |  322 +++
 cloudformation_templates/ec2-or-emr-vpc.yaml       |  283 +++
 clouds/__init__.py                                 |   16 +
 clouds/aws.py                                      |  385 ++++
 constant/__init__.py                               |   16 +
 constant/client.py                                 |   24 +
 constant/commands.py                               |   49 +
 constant/config.py                                 |   65 +
 constant/deployment.py                             |   40 +
 constant/kylin_properties_params.py                |   29 +
 constant/path.py                                   |   34 +
 constant/server_mode.py                            |   24 +
 constant/yaml_files.py                             |   43 +
 constant/yaml_params.py                            |   92 +
 deploy.py                                          |   98 +
 engine.py                                          |  135 ++
 engine_utils.py                                    |  255 +++
 images/accesskey.png                               |  Bin 0 -> 218426 bytes
 images/accesskey2.png                              |  Bin 0 -> 47858 bytes
 images/directory.png                               |  Bin 0 -> 80810 bytes
 images/granfana2.png                               |  Bin 0 -> 111591 bytes
 images/granfana3.png                               |  Bin 0 -> 91465 bytes
 images/granfanadashboard.png                       |  Bin 0 -> 218488 bytes
 images/granfanadatasource.png                      |  Bin 0 -> 67888 bytes
 images/granfanaeditpanel.png                       |  Bin 0 -> 132924 bytes
 images/granfanapanel.png                           |  Bin 0 -> 55420 bytes
 images/jars.png                                    |  Bin 0 -> 132486 bytes
 images/keypair.png                                 |  Bin 0 -> 112593 bytes
 images/kylinproperties.png                         |  Bin 0 -> 23382 bytes
 images/newpanel.png                                |  Bin 0 -> 93973 bytes
 images/prometheusmetrics.png                       |  Bin 0 -> 92838 bytes
 images/prometheusquery.png                         |  Bin 0 -> 110615 bytes
 images/renderedkylinproperties.png                 |  Bin 0 -> 24685 bytes
 images/scripts.png                                 |  Bin 0 -> 144342 bytes
 images/sparkmetrics.png                            |  Bin 0 -> 231429 bytes
 images/staticserviceparam.png                      |  Bin 0 -> 89601 bytes
 images/structure.png                               |  Bin 0 -> 79664 bytes
 images/tars.jpg                                    |  Bin 0 -> 219568 bytes
 images/tars.png                                    |  Bin 0 -> 189254 bytes
 instances/__init__.py                              |   16 +
 instances/aws_instance.py                          | 2163 ++++++++++++++++++++
 instances/kylin_instance.py                        |   26 +
 instances/kylin_utils.py                           |   39 +
 kylin_configs.yaml                                 |  306 +++
 logging.ini                                        |   47 +
 logs/.keepme                                       |   16 +
 requirements.txt                                   |   25 +
 utils.py                                           |  188 ++
 69 files changed, 10259 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..87514ad
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+*/__pycache__/
+*/.DS_Store
+/venv/
+.idea
+logs/*.log
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..0ab0f0e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,574 @@
+# How to deploy a Kylin4 Cluster on EC2
+
+## Target
+
+1. Deploy Kylin4 on Ec2 with Spark Standalone mode.
+2. Removed the dependency of hadoop and start quickly.
+3. Support to scale worker nodes for Spark Standalone Cluster quickly and conveniently.
+4. Improve performance for query in using  `Local Cache + Soft Affinity` feature (`Experimental Feature`), please check the [details](https://mp.weixin.qq.com/s/jEPvWJwSClQcMLPm64s4fQ).
+5. Support to monitor cluster status with prometheus server and granfana.
+6. Create a Kylin4 cluster on EC2 in 10 minutes.
+
+## Structure
+
+When cluster was created, services and nodes will like below:
+
+![structure](./images/structure.png)
+
+- Services are created as the number order from 1 to 4.
+- Every machine node is presented by a white box. 
+- `Kylin Node` and `Spark Worker` Node can be scaled.
+- Whole cluster will has only one RDS and only one the machine node which contains `Prometheus Server` and `Hive MetaStore` service.
+
+## Quick Start
+
+1. Initialize aws account credential on local mac, please check [details](#localaws).
+   
+2. Download the source code: 
+   
+   ```shell
+   git clone https://github.com/Kyligence/kylin-tpch.git && cd kylin-tpch && git checkout deploy-kylin-on-aws
+   ```
+   
+3. Modify the `kylin-tpch/kylin_config.yml`.
+   
+   1. Set the `AWS_REGION`.
+   
+   2. Set the `IAMRole`,please check [details](#IAM).
+   
+   3. Set the `S3_URI`, please check [details](#S3).
+   
+   4. Set the `KeyName`,please check [details](#keypair).
+   
+   5. Set the `CIDR_IP`, make sure that the `CIDR_IP` match the pattern `xxx.xxx.xxx.xxx/16[|24|32]`.
+   
+      > Note: 
+      >
+      > 1. this `CIDR_IP` is the specified IPv4 or IPv6 CIDR address range which an inbound rule can permit instances to receive traffic from.
+      >
+      > 2. In one word, it will let your mac which ip is in the `CIDR_IP` to access instances.
+   
+4. Init local env.
+
+```She
+$KYLIN_TPCH_HOME/bin/init.sh
+```
+
+> Note: Following the information into a python virtual env and get the help messages. 
+
+5. Execute commands to deploy a `default` cluster.
+
+```shell
+$ python ./deploy.py --type deploy
+```
+
+After `default` cluster is ready, you will see the message `Kylin Cluster already start successfully.` in the console. 
+
+6. Execute commands to list nodes of cluster.
+
+```shell
+$ python ./deploy.py --type list
+```
+
+Then you can check the `public ip` of Kylin Node.
+
+You can access `Kylin` web by `http://{kylin public ip}:7070/kylin`.
+
+7. Destroy the `default` cluster.
+
+```shell
+$ python ./deploy.py --type destroy
+```
+
+
+
+## Quick Start For Multiple Clusters
+
+> Pre-steps is same as Quick Start steps which is from 1 to 5.
+
+1. Modify the config `CLUSTER_INDEXES` for multiple cluster.
+
+   > Note:
+   >
+   > 1. `CLUSTER_INDEXES` means that cluster index is in the range of `CLUSTER_INDEXES`. 
+   > 2. If user create multiple clusters, `default` cluster always be created. If `CLUSTER_INDEXES` is (1, 3), there will be 4 cluster which contains the cluster 1, 2, 3 and `default` will be created if user execute the commands.
+   > 3. Configs for multiple clusters always are same as the `default` cluster to read from `kylin-tpch/kylin_configs.yaml`
+
+2. Copy `kylin.properties.template` for expecting clusters to deploy, please check the [details](#cluster). 
+
+3. Execute commands to deploy `all` clusters.
+
+   ```shell
+   python ./deploy.py --type deploy --cluster all
+   ```
+
+4. Destroy all clusters.
+
+   ```shell
+   python ./deploy.py --type destroy --cluster all
+   ```
+
+
+
+## Prerequisites 
+
+### Download source code & checkout to branch of `deploy-kylin-on-aws`
+
+commands:
+
+```shell
+git clone https://github.com/Kyligence/kylin-tpch.git && cd kylin-tpch && git checkout deploy-kylin-on-aws
+```
+
+### Initiliaze an AWS Account
+
+#### I. Create an `IAM` role<a name="IAM"></a> 
+
+> Note: 
+>
+> ​	`IAM` role must have the access  which contains `AmazonEC2RoleforSSM` , `AmazonSSMFullAccess` and `AmazonSSMManagedInstanceCore`.
+>
+> ​	This `IAM` Role will be used to initialize every ec2 instances which are for creating an kylin4 cluster on aws. And it will configure in `Initilize Env of Local Mac` part.
+
+#### II. Create a `User` 
+
+> Note:
+>
+> ​	The `User` who is created in aws will be used to execute some operations on EC2 instance. So the `User` must has some authorities as below.
+
+| Services            | Access level                                           | Resources     | Request condition |
+| ------------------- | ------------------------------------------------------ | ------------- | ----------------- |
+| **Cloudformation**  | **Limited**: List, Read,Write                          | All Resources | None              |
+| **EC2**             | **Limited**: Read                                      | All Resources | None              |
+| **IAM**             | **Limited**: List, Read, Write, Permissions management | All Resources | None              |
+| **RDS**             | **Limited**: List, Write, Tagging                      | All Resources | None              |
+| **S3**              | **Limited**: Write, Permissions management, Tagging    | All Resources | None              |
+| **SNS**             | **Limited**: List                                      | All Resources | None              |
+| **STS**             | **Limited**: Write                                     | All Resources | None              |
+| **Systems Manager** | **Limited**: Write                                     | All Resources | None              |
+
+#### III. Login the created `User` to create key pair and Access Key<a name="keypair"></a>
+
+> Note:
+>
+> ​	To deploy EC2 instances for Kylin4 Cluster need key pair and access key.
+
+##### 1. Create a new `Key Pair` in the `EC2 -> Key pairs`
+
+![key pair](./images/keypair.png)
+
+##### 2. Genrate a new `Access Key` which in the `My Security Credentials`
+
+> Note: 
+>
+> ​	Please download the generated the csv file of `Access Key`  immediately. Get the `Access Key `  and `Secret Key` to initilize local mac to access aws.
+
+![Access Key](./images/accesskey.png)
+
+
+
+![Access Key 2](./images/accesskey2.png)
+
+
+
+### Initialize a S3 direcotry<a name="S3"></a> 
+
+#### I. Create a directory on S3
+
+> Note: 
+>
+> ​	This directory will store tars, scripts and jars below. And it will also be the root path of working dir for kylin4.
+
+Example: make a directory named `kylin4-aws-test` . You can also create a directory named what you like.
+
+![directory](./images/directory.png)
+
+
+
+#### **(Optional)** II. Manually Download packages & Upload them to the S3 path which suffix is `*/tar`
+
+> Note:
+>
+> ​	This step will automatically do by tools. So you can skip this step, or you want to check packages by yourself.
+
+
+
+> Create the directory named `tar` **in the path which was created by yourself**.  
+>
+> As example, the full path would be `s3://.../kylin4-aws-test/tar`.
+
+1. Download Kylin4 package by [official website](https://kylin.apache.org/download/).
+2. Download Hadoop, [version 3.2.0](https://archive.apache.org/dist/hadoop/common/hadoop-3.2.0/hadoop-3.2.0.tar.gz).
+3. Download Spark with hadoop3.2, [version 3.1.1](https://archive.apache.org/dist/spark/spark-3.1.1/spark-3.1.1-bin-hadoop3.2.tgz).
+4. Download Hive, [version 2.3.9](https://archive.apache.org/dist/hive/hive-2.3.9/apache-hive-2.3.9-bin.tar.gz).
+5. Download Zookeeper, [version 3.4.9.](https://archive.apache.org/dist/zookeeper/zookeeper-3.4.9/zookeeper-3.4.9.tar.gz)
+6. Download JDK, [version 1.8_301](https://www.oracle.com/java/technologies/javase/javase8u211-later-archive-downloads.html).
+7. Download Node Exporter, [version 1.3.1.](https://github.com/prometheus/node_exporter/releases/download/v1.3.1/node_exporter-1.3.1.linux-amd64.tar.gz)
+8. Download Prometheus Server, [version 2.31.1](https://github.com/prometheus/prometheus/releases/download/v2.31.1/prometheus-2.31.1.linux-amd64.tar.gz).
+9. Download Kylin4 package with local cache + soft affinity feature by [public website.](https://s3.cn-north-1.amazonaws.com.cn/public.kyligence.io/kylin/tar/apache-kylin-4.0.0-bin-spark3-soft.tar.gz)
+
+
+
+> Note: 
+>   	If you want to use Kylin4 with local cache + soft affinity feature, please download the `experimental` package above.
+
+![tars](./images/tars.png)
+
+
+
+#### (Optional) III. Upload  `kylin-tpch/backup/jars/*` to the S3 Path which suffix is `*/jars`
+
+> Note:
+>
+> ​	This step will automatically do by tools. So you can skip this step, or you want to check jars by yourself.
+
+
+
+> Create the directory named `jars` **in the path which was created by yourself**.  
+>
+> As example, the full path would be `s3://.../kylin4-aws-test/jars`.
+
+Kylin4 needed extra jars
+
+- Basic jars
+    - commons-configuration-1.3.jar
+    - mysql-connector-java-5.1.40.jar
+- Local Cache + Soft Affinity feature needed jars
+    - alluxio-2.6.1-client.jar
+    - kylin-soft-affinity-cache-4.0.0-SNAPSHOT.jar
+
+![jars](./images/jars.png)
+
+#### (Optional) IV. Upload `kylin-tpch/backup/scripts/*` to the S3 Path which suffix is `*/scripts`
+
+> Note:
+>
+> ​	This step will automatically do by tools. So you can skip this step, or you want to check scripts by yourself.
+
+
+
+> Create the directory named `scripts` **in the path which was created by yourself**.  
+>
+> As example, the full path would be `s3://.../kylin4-aws-test/scripts`.
+
+Scripts:
+
+- prepare-ec2-env-for-kylin4.sh
+- prepare-ec2-env-for-spark-master.sh
+- prepare-ec2-env-for-spark-slave.sh
+- prepare-ec2-env-for-static-services.sh
+- prepare-ec2-env-for-zk.sh
+
+![scripts](./images/scripts.png)
+
+### Initilize Env Of Local Mac
+
+#### I.  Initilize a aws account on local mac to access AWS<a name="localaws"></a>
+
+> Use `Access Key` and `Secret Key ` above to Initilize a aws account on local mac. 
+
+```shell
+$ aws configure
+AWS Access Key ID : *************
+AWS Secret Access Key : *************
+Default region name : cn-northwest-1
+Default output format : json
+```
+
+> Note:
+>
+> 1. If this command got the response `-bash: aws: command not found`, please check in  [Getting started with the AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html).
+> 2. Region name can be in [Available Regions](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#concepts-available-regions).
+> 3. Default output format is `json`.
+
+#### II. Init a Python virtual env
+
+> Note: 
+>
+> ​	Make sure that your mac already has a Python which version is 3.6.6 or later.  
+
+commands: 
+
+```shell
+$ ./bin/init.sh
+```
+
+> Note: Follow the information after `./bin/init.sh` to activate the python virtual env.
+
+#### III. Configure the `kylin-tpch/kylin_configs.yaml`
+
+**Required parameters**:
+
+- `AWS_REGION`: Current region for EC2 instances.
+- `IAMRole`: IAM role which has the access to aws authority. This parameter will be set to created **name** of IAM role.
+- `S3_URI`: the prefix path of storing `jars/scripts/tar`. As example, this parameter will be set to `s3://.../kylin4-aws-test`.
+- `KeyName`: Security key name is a set of security credentials that you use to prove your identity when connecting to an instance. This parameter will be set to created **name** of  `key pair` .
+- `CIDR_IP`:  An inbound rule permits instances to receive traffic from the specified IPv4 or IPv6 CIDR address range, or from the instances associated with the specified security group.
+- `DB_IDENTIFIER`: this param should be only one in the `RDS -> Databases` . And it will be the name of created RDS database.
+- `DB_PORT`: this param will be the port of created RDS database, default is `3306`.
+- `DB_USER`: this param will be a login ID for the master user of your DB instance, default is `root`.
+- `DB_PASSWORD`: this param will be the password of `DB_USER` to access DB instance. default is `123456test`, it's strongly suggest you to change it.
+
+#### IV. Configure the `kylin.properties` in `backup/properties` directories.<a name="cluster"></a>
+
+1. The `kylin.properties` is for starting kylin instance in the cluster.
+2. Default cluster will check the `kylin.properties` in the `kylin-tpch/backup/properties/default`, and other specific cluster will check the related num directory such as `1`, `2` and `3`.
+3. User need to create new dir for the cluster num in `kylin-tpch/backup/properties`, and name it to the `${cluster num}`, such as `1`, `2` ,`3`  and so on. The range of cluster num must be in `CLUSTER_INDEXES` which is configured in the `kylin-tpch/kylin_configs.yml`.
+4. Follow the `2.` step, copy the `kylin.properties.template` which is in `kylin-tpch/backup/properties/templates` to the related `${cluster num} ` directories, and rename the template to `kylin.properties`. 
+5. The range of cluster nums must match the the config `CLUSTER_INDEXES`, such as `CLUSTER_INDEXES: (1, 3)` then the directories must be `1`, `2`,`3` in the `kylin-tpch/backup/properties`.
+
+![kylin properties](./images/kylinproperties.png)
+
+6. Cusomize the config which you want to change except the config which contains `{{ }}` mark.
+
+7. If the related `kylin.properties` was filled with cluster information, a file named `.rendered` will create in the directory.
+
+![rendered kylin properties](./images/renderedkylinproperties.png)
+
+7. **(Important !!!)** If you want to re-fill the `kylin.properties` for a `kylin` node in specify cluster,  you need to remove the `.rendered` file and re-copy the `kylin.propertie.template` . Redo steps from `3` to `6`.
+
+## Advanced Configs
+
+> Note:
+>
+> ​	If you want quickly to start Kylin4 Cluster on aws, then skip this part and jump to the part of  [`Run`](#run) directly. 
+
+### Advaced Params
+
+There are `9` modules params for tools.  Introductions as below:
+
+- EC2_VPC_PARAMS: this params of module are for creating a vpc.
+
+- EC2_RDS_PARAMS: this params of module are for creating a RDS.
+
+- EC2_STATIC_SERVICES_PARAMS: this params of module are for creating a Prometheus Server and other static services.
+
+- EC2_ZOOKEEPERS_PARAMS: this params of module are for creating a Zookeeper Cluster.
+
+- EC2_SPARK_MASTER_PARAMS: this params of module are for creating a Spark Master node.
+
+- EC2_KYLIN4_PARAMS: this params of module are for creating a Kylin4.
+
+- EC2_SPARK_WORKER_PARAMS: this params of module are for creating **Spark Workers**, default is **3** spark workers for all clusters.
+
+- EC2_KYLIN4_SCALE_PARAMS: this params of module are for scaling **Kylin4 nodes**, the range of **Kylin4 nodes** is related to `KYLIN_SCALE_UP_NODES` and `KYLIN_SCALE_DOWN_NODES`.
+
+  > Note:
+  >
+  > 	1. `KYLIN_SCALE_UP_NODES` is for the range of kylin nodes to scale up. 
+  > 	1. `KYLIN_SCALE_DOWN_NODES` is for the range of kylin nodes to scale down.
+  > 	1. The range of `KYLIN_SCALE_UP_NODES` must be contain the range of `KYLIN_SCALE_DOWN_NODES`.
+  > 	1. **They are effective to all clusters which is not only `default cluster` but also other cluster which index is in `${CLUSTER_INDEXES}`.**
+
+- EC2_SPARK_SCALE_SLAVE_PARAMS: this params of module are for scaling **Spark workers**, the range of **Spark Workers ** is related to `SPARK_WORKER_SCALE_UP_NODES` and `SPARK_WORKER_SCALE_DOWN_NODES`.
+
+  > Note:
+  >
+  > 	1. `SPARK_WORKER_SCALE_UP_NODES` is for the range for spark workers to scale up. **It's effective to all clusters which is not only `default cluster` but also other cluster which index is in `${CLUSTER_INDEXES}`.**
+  > 	1. `SPARK_WORKER_SCALE_DOWN_NODES` is for the range for spark workers to scale down. **It's effective to all clusters which is not only `default cluster` but also other cluster which index is in `${CLUSTER_INDEXES}`.**
+  > 	1. The range of `SPARK_WORKER_SCALE_UP_NODES` must be contain the range of `SPARK_WORKER_SCALE_DOWN_NODES`.
+  > 	1. **They are effective to all clusters which is not only `default cluster` but also other cluster which index is in `${CLUSTER_INDEXES}`.**
+
+### Customize Configs
+
+User also can customize the params in `kylin-tpch/kylin_configs.yaml` to create an expected instances. Such as **the type of instance**, **the volume size of instance** and **the volumn type of instance** and so on.
+
+1. If you want to customize configs for instances, you must modify the `EC2Mode` from `test` to `product` in the ``kylin-tpch/kylin_configs.yml`.
+2. `Ec2Mode` is only in the parms of `EC2_STATIC_SERVICES_PARAMS`, `EC2_ZOOKEEPERS_PARAMS`, `EC2_SPARK_MASTER_PARAMS`, `EC2_KYLIN4_PARAMS`, `EC2_SPARK_WORKER_PARAMS`, `EC2_KYLIN4_SCALE_PARAMS` and `EC2_SPARK_SCALE_SLAVE_PARAMS`.
+3. So instances can be customized to effect `Monitor Node`(`EC2_STATIC_SERVICES_PARAMS`), `Zookeeper Nodes`(`EC2_ZOOKEEPERS_PARAMS`), `Spark Master Node` ( `EC2_SPARK_MASTER_PARAMS`), `Kylin4 Node`( `EC2_KYLIN4_PARAMS`), `Spark workers `(`EC2_SPARK_WORKER_PARAMS`), `Kylin4 scale nodes`(`EC2_KYLIN4_SCALE_PARAMS`) and `Spark scale workers`(`EC2_SPARK_SCALE_SLAVE_PARAMS`).
+4. Now`Ec2Mode` **only effect** the related params are `Ec2InstanceTypeFor*`,`Ec2VolumeSizeFor*`  and `Ec2VolumnTypeFor`* in the params modules.
+5. If you don't change `ENABLE_LOCAL_CACHE_SOFT_AFFINITY` from `"false"` to `"true"` then cluster will created normally without `Local Cache + Soft Affinity` feature!
+
+
+
+#### Example
+
+As an example in `EC2_STATIC_SERVICES_PARAMS`:
+
+- change `Ec2Mode `  from `test`to `product`
+- change `Ec2InstanceTypeForStaticServices`  from `m5.2xlarge` to `m5.4xlarge`.
+- change `Ec2VolumeSizeForStaticServicesNode`  from `'20'` to `'50'.`
+- change `Ec2VolumnTypeForStaticServicesNode` from `gp2` to `standard`.
+- Then create the node of static service node will be a ``m5.4xlarge` and it attach a volume which size is `50` and type is `standard`.
+
+![static service params](./images/staticserviceparam.png)
+
+
+
+## Monitor
+
+Current tool also support the feature of monitor for cluster.
+
+1. User can execute `python ./deploy.py --type list` to get the `public ip` of `Static Service` .
+2. User can access the `public ip` of `Static Service` and the port is `9090` to access `Prometheus` server.
+3. User can access the `public ip` of `Static Service` and the port is `3000` to access `Granfana` server.
+
+### Quick Start For Prometheus
+
+1. Input a `Metric`, user can get the available metric from the metric exploer.
+
+![prometheus metrics](./images/prometheusmetrics.png)
+
+2. Execute a query to generate a dashboad.
+
+![prometheus query](./images/prometheusquery.png)
+
+3. Add a new panel for a new query.
+
+![new panel](./images/newpanel.png)
+
+More details about `PromQL` for metric exploer, please check [official website](https://prometheus.io/docs/prometheus/latest/querying/).
+
+#### Spark Metric In Prometheus
+
+>  Note:
+>
+> 1. Current tool support to monitor spark metrics, more detail in [Spark Metrics](https://spark.apache.org/docs/3.1.1/monitoring.html#executor-metrics).
+> 2. Spark metrics is start with `metrics` in Prometheus server.
+>
+> ![spark metrics](./images/sparkmetrics.png)
+
+ ### Quick Start For Granfana
+
+1. Login into dashboard with default user and password which are all `admin` at the first time.
+2. Configure the connection to prometheus server.
+
+![granfana datasource](./images/granfanadatasource.png)
+
+![granfana 2](./images/granfana2.png)
+
+![granfana 3](./images/granfana3.png)
+
+> Just config the url with syntax `http://${private ip of static service}:9090` . The `private ip of static service` can be from the `python ./deploy.py --type list` command.
+
+3. Add a new panel.
+
+![granfana dashboard](./images/granfanadashboard.png)
+
+4. Add a new panel or other.
+
+![granfana panel](./images/granfanapanel.png)
+
+5. Edit & Apply the panel.
+
+![granfana edit panel](./images/granfanaeditpanel.png)
+
+More details about the usage of Granfana in [official website](https://grafana.com/docs/grafana/latest/).
+
+## Run<a name="run"></a>
+
+Command:
+
+```shell
+python ./deploy.py --type [deploy|destroy|list|scale] --scale-type [up|down] --node-type [kylin|spark_worker] [--cluster {1..6}|all|default]
+```
+
+- deploy: create cluster(s).
+
+- destroy: destroy created cluster(s).
+
+- list: list alive nodes which are with stack name, instance id, private ip and public ip.
+
+- scale: Must be used with `--scale-type` and `--node-type`.
+
+  > Note:
+  >
+  > 1. Current support to scale up/down `kylin` or `spark_worker` for specific cluster.
+  > 2. Before scale up/down `kylin` or `spark_worker` nodes, Cluster services must be ready.
+  > 2. If you want to scale a `kylin` or `spark_worker` node to a specify cluster, please add the `--cluster ${cluster num}` to specify the expected node add to the cluster `${cluster num}`.
+
+### Command of deploy
+
+- Deploy default cluster
+
+```shell
+$ python ./deploy.py --type deploy [--cluster default]
+```
+
+- Deploy a cluster with specific cluster index. 
+
+```shell
+$ python ./deploy.py --type deploy --cluster ${cluster num}
+```
+
+> Note: the `${cluster num}` must be in the range of `CLUSTER_INDEXES`.
+
+- Deploy all cluster which contain default cluster and all cluster which index in the range of `CLUSTER_INDEXES`.
+
+```sHe
+$ python ./deploy.py --type deploy --cluster all
+```
+
+### Command of destroy
+
+> Note:
+>
+> ​		Destroy all cluster will not delete vpc, rds and monitor node. So if user don't want to hold the env, please set the `ALWAYS_DESTROY_ALL` to be `'true'`.
+
+- Destroy default cluster
+
+```shell
+$ python ./deploy.py --type destroy [--cluster default]
+```
+
+- Destroy a cluster with specific cluster index. 
+
+```shell
+$ python ./deploy.py --type destroy --cluster ${cluster num}
+```
+
+> Note: the `${cluster num}` must be in the range of `CLUSTER_INDEXES`.
+
+- Destroy all cluster which contain default cluster and all cluster which index in the range of `CLUSTER_INDEXES`.
+
+```shell
+$ python ./deploy.py --type destroy --cluster all
+```
+
+### Command of list
+
+- List nodes which are with **stack name**, **instance id**, **private ip** and **public ip** in **available stacks** .
+
+```shell
+$ python ./deploy.py --type list
+```
+
+### Command of scale
+
+> Note:
+>
+> 1. Scale command must be used with `--scale-type` and `--node-type`.
+> 2. If scale command not specify a cluster num, then the scaled node(kylin or spark worker) will be add to `default`cluster.
+> 3. Scale command **not support** to **scale** node (kylin or spark worker) to **all clusters** at **one time**. It means that `python ./deploy.py --type scale --scale-type up[|down] --node-type kylin[|spark_worker] --cluster all` is invalid commad.
+> 4. Scale params which are `KYLIN_SCALE_UP_NODES`, `KYLIN_SCALE_DOWN_NODES`, `SPARK_WORKER_SCALE_UP_NODES` and `SPARK_WORKER_SCALE_DOWN_NODES` effect on all cluster. So if user want to scale node for a specify cluster, then modify the scale params before **every run time.**
+> 5. **(Important!!!)** Current cluster is created with default `3` spark workers and `1` kylin node. The `3` spark workers can not be scaled down. The `1`  kylin node also can not be scaled down.
+> 6. **(Important!!!)** Cluster can only scale up or down the range of nodes which is in  `KYLIN_SCALE_UP_NODES`, `KYLIN_SCALE_DOWN_NODES`, `SPARK_WORKER_SCALE_UP_NODES` and `SPARK_WORKER_SCALE_DOWN_NODES` . Not the default `3` spark workers and `1` kylin node in the cluster.
+> 7. **(Important!!!)**  If user don't want to create a cluster with `3` default spark workers, then user can remove the useless node module in the `Ec2InstanceOfSlave0*` of `kylin-tpch/cloudformation_templates/ec2-cluster-spark-slave.yaml`. User need to know about the syntax of cloudformation as also.
+
+- Scale up/down kylin/spark workers in default cluster
+
+```shell
+python ./deploy.py --type scale --scale-type up[|down] --node-type kylin[|spark_worker] [--cluster default]
+```
+
+- Scale up/down kylin/spark workers in a specific cluster
+
+```shell
+python ./deploy.py --type scale --scale-type up[|down] --node-type kylin[|spark_worker] --cluster ${cluster num}
+```
+
+> Note: the `${cluster num}` must be in the range of `CLUSTER_INDEXES`.
+
+
+
+## Notes
+
+1. Current tool already open the port for some services. You can access the service by `public ip` of related EC2 instance.
+   1. `SSH`: 22
+   2. `Granfana`:  3000
+   3. `Prmetheus`:  9090, 9100
+   4. `Kylin`: 7070
+   5. `Spark`: 8080. 4040.
+2. More about cloudformation syntax, please check [aws website](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/Welcome.html).
+3. Current Kylin version is 4.0.0.
+4. Current Spark version is 3.1.1.
diff --git a/__init__.py b/__init__.py
new file mode 100644
index 0000000..cce3aca
--- /dev/null
+++ b/__init__.py
@@ -0,0 +1,16 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
diff --git a/backup/properties/default/kylin.properties b/backup/properties/default/kylin.properties
new file mode 100644
index 0000000..f800b5d
--- /dev/null
+++ b/backup/properties/default/kylin.properties
@@ -0,0 +1,67 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+
+# Kylin server mode, valid value [all, query, job]
+kylin.server.mode=all
+kylin.metadata.url=kylin_metadata@jdbc,url=jdbc:mysql://{{ DB_HOST }}:{{ DB_PORT }}/kylin,username=root,password={{ DB_PASSWORD }},maxActive=10,maxIdle=10
+kylin.env.zookeeper-connect-string={{ ZOOKEEPER_HOST }}
+kylin.env.hdfs-working-dir=s3a:/{{ S3_BUCKET_PATH }}/working_dir/
+## Build Engine Resource
+kylin.engine.spark-conf.spark.eventLog.dir=s3a:/{{ S3_BUCKET_PATH }}/working_dir/spark-history
+kylin.engine.spark-conf.spark.history.fs.logDirectory=s3a:/{{ S3_BUCKET_PATH }}/working_dir/spark-history
+kylin.engine.spark-conf.spark.master=spark://{{ SPARK_MASTER }}:7077
+
+kylin.cube.cubeplanner.enabled=true
+kylin.engine.spark-conf.spark.executor.cores=3
+kylin.engine.spark-conf.spark.executor.instances=20
+kylin.engine.spark-conf.spark.executor.memory=12GB
+kylin.engine.spark-conf.spark.executor.memoryOverhead=1GB
+
+### support prometheus
+kylin.engine.spark-conf.spark.ui.prometheus.enabled=true
+kylin.engine.spark-conf.spark.executor.processTreeMetrics.enabled=true
+
+## Parquet Column Index
+kylin.engine.spark-conf.spark.hadoop.parquet.page.size=1048576
+kylin.engine.spark-conf.spark.hadoop.parquet.page.row.count.limit=100000
+kylin.engine.spark-conf.spark.hadoop.parquet.block.size=268435456
+kylin.query.spark-conf.spark.hadoop.parquet.filter.columnindex.enabled=true
+
+## Query Engine Resource
+kylin.query.spark-conf.spark.master=spark://{{ SPARK_MASTER }}:7077
+kylin.query.spark-conf.spark.driver.cores=1
+kylin.query.spark-conf.spark.driver.memory=8GB
+kylin.query.spark-conf.spark.driver.memoryOverhead=1G
+kylin.query.spark-conf.spark.executor.instances=30
+kylin.query.spark-conf.spark.executor.cores=2
+kylin.query.spark-conf.spark.executor.memory=7G
+kylin.query.spark-conf.spark.executor.memoryOverhead=1G
+kylin.query.spark-conf.spark.sql.parquet.filterPushdown=false
+
+### support prometheus
+kylin.query.spark-conf.spark.ui.prometheus.enabled=true
+kylin.query.spark-conf.spark.executor.processTreeMetrics.enabled=true
+
+## Disable canary
+kylin.canary.sparder-context-canary-enabled=false
+## Query Cache
+kylin.query.cache-enabled=true
+
+### Prepare for cluster mode
+kylin.job.scheduler.default=100
+kylin.server.self-discovery-enabled=true
\ No newline at end of file
diff --git a/backup/properties/templates/kylin.properties.template b/backup/properties/templates/kylin.properties.template
new file mode 100644
index 0000000..94dd2dd
--- /dev/null
+++ b/backup/properties/templates/kylin.properties.template
@@ -0,0 +1,65 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Kylin server mode, valid value [all, query, job]
+kylin.server.mode=all
+kylin.metadata.url=kylin_metadata@jdbc,url=jdbc:mysql://{{ DB_HOST }}:{{ DB_PORT }}/kylin,username=root,password={{ DB_PASSWORD }},maxActive=10,maxIdle=10
+kylin.env.zookeeper-connect-string={{ ZOOKEEPER_HOST }}
+kylin.env.hdfs-working-dir=s3a:/{{ S3_BUCKET_PATH }}/working_dir/
+## Build Engine Resource
+kylin.engine.spark-conf.spark.eventLog.dir=s3a:/{{ S3_BUCKET_PATH }}/working_dir/spark-history
+kylin.engine.spark-conf.spark.history.fs.logDirectory=s3a:/{{ S3_BUCKET_PATH }}/working_dir/spark-history
+kylin.engine.spark-conf.spark.master=spark://{{ SPARK_MASTER }}:7077
+
+kylin.cube.cubeplanner.enabled=true
+kylin.engine.spark-conf.spark.executor.cores=3
+kylin.engine.spark-conf.spark.executor.instances=20
+kylin.engine.spark-conf.spark.executor.memory=12GB
+kylin.engine.spark-conf.spark.executor.memoryOverhead=1GB
+
+### support prometheus
+kylin.engine.spark-conf.spark.ui.prometheus.enabled=true
+kylin.engine.spark-conf.spark.executor.processTreeMetrics.enabled=true
+
+## Parquet Column Index
+kylin.engine.spark-conf.spark.hadoop.parquet.page.size=1048576
+kylin.engine.spark-conf.spark.hadoop.parquet.page.row.count.limit=100000
+kylin.engine.spark-conf.spark.hadoop.parquet.block.size=268435456
+kylin.query.spark-conf.spark.hadoop.parquet.filter.columnindex.enabled=true
+
+## Query Engine Resource
+kylin.query.spark-conf.spark.master=spark://{{ SPARK_MASTER }}:7077
+kylin.query.spark-conf.spark.driver.cores=1
+kylin.query.spark-conf.spark.driver.memory=8GB
+kylin.query.spark-conf.spark.driver.memoryOverhead=1G
+kylin.query.spark-conf.spark.executor.instances=30
+kylin.query.spark-conf.spark.executor.cores=2
+kylin.query.spark-conf.spark.executor.memory=7G
+kylin.query.spark-conf.spark.executor.memoryOverhead=1G
+kylin.query.spark-conf.spark.sql.parquet.filterPushdown=false
+
+### support prometheus
+kylin.query.spark-conf.spark.ui.prometheus.enabled=true
+kylin.query.spark-conf.spark.executor.processTreeMetrics.enabled=true
+
+## Disable canary
+kylin.canary.sparder-context-canary-enabled=false
+## Query Cache
+kylin.query.cache-enabled=true
+
+### Prepare for cluster mode
+kylin.job.scheduler.default=100
+kylin.server.self-discovery-enabled=true
\ No newline at end of file
diff --git a/backup/scripts/prepare-ec2-env-for-kylin4.sh b/backup/scripts/prepare-ec2-env-for-kylin4.sh
new file mode 100644
index 0000000..54a3d6d
--- /dev/null
+++ b/backup/scripts/prepare-ec2-env-for-kylin4.sh
@@ -0,0 +1,755 @@
+#!/bin/bash
+
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# Note: this script is for AWS ec2 instances
+set -e
+
+# ============= Utils function ============
+function info() {
+  # shellcheck disable=SC2145
+  echo -e "\033[32m$@\033[0m"
+}
+
+function warn() {
+  # shellcheck disable=SC2145
+  echo -e "\033[33m$@\033[0m"
+}
+
+function error() {
+  # shellcheck disable=SC2145
+  echo -e "\033[31m$@\033[0m"
+}
+
+function logging() {
+  case $1 in
+  "info")
+    shift
+    # shellcheck disable=SC2068
+    info $@
+    ;;
+  "warn")
+    shift
+    # shellcheck disable=SC2068
+    warn $@
+    ;;
+  "error")
+    shift
+    # shellcheck disable=SC2068
+    error $@
+    ;;
+  *)
+    # shellcheck disable=SC2068
+    echo -e $@
+    ;;
+  esac
+}
+
+set +e
+
+function help() {
+  logging warn "Invalid input."
+  logging warn "Usage: ${BASH_SOURCE[0]}
+                    --bucket-url /path/to/bucket/without/prefix
+                    --region region-for-s3
+                    --db-host db-host-for-kylin
+                    --db-password db-password-for-kylin
+                    --db-port db-port-for-kylin
+                    --db-user db-user-for-kylin
+                    --kylin-mode mode-for-kylin[all|query|job]
+                    --local-soft whether-to-use-local-cache+soft-affinity
+                    --cluster-num specify-a-cluster"
+  exit 0
+}
+
+if [[ $# -ne 18 ]]; then
+  help
+fi
+
+while [[ $# != 0 ]]; do
+  if [[ $1 == "--bucket-url" ]]; then
+    # url same as: /xxx/kylin
+    BUCKET_SUFFIX=$2
+  elif [[ $1 == "--region" ]]; then
+    CURRENT_REGION=$2
+  elif [[ $1 == "--db-host" ]]; then
+    DATABASE_HOST=$2
+  elif [[ $1 == "--db-password" ]]; then
+    DATABASE_PASSWORD=$2
+  elif [[ $1 == "--db-user" ]]; then
+    DATABASE_USER=$2
+  elif [[ $1 == '--db-port' ]]; then
+    DATABASE_PORT=$2
+  elif [[ $1 == "--local-soft" ]]; then
+    LOCAL_CACHE_SOFT_AFFINITY=$2
+  elif [[ $1 == '--cluster-num' ]]; then
+    # default value is 'default', and cluster num is from 1 to positive infinity.
+    CLUSTER_NUM=$2
+  elif [[ $1 == '--is-scaled' ]]; then
+    IS_SCALED=$2
+  else
+    help
+  fi
+  shift
+  shift
+done
+
+# =============== Env Parameters =================
+# Prepare Steps
+### Parameters for Spark and Kylin
+#### ${SPARK_VERSION:0:1} get 2 from 2.4.7
+HADOOP_VERSION=3.2.0
+SPARK_VERSION=3.1.1
+KYLIN_VERSION=4.0.0
+HIVE_VERSION=2.3.9
+
+LOCAL_CACHE_DIR=/home/ec2-user/ssd
+
+### File name
+if [[ $LOCAL_CACHE_SOFT_AFFINITY == "false" ]]; then
+  KYLIN_PACKAGE=apache-kylin-${KYLIN_VERSION}-bin-spark${SPARK_VERSION:0:1}.tar.gz
+  DECOMPRESSED_KYLIN_PACKAGE=apache-kylin-${KYLIN_VERSION}-bin-spark${SPARK_VERSION:0:1}
+else
+  KYLIN_PACKAGE=apache-kylin-${KYLIN_VERSION}-bin-spark${SPARK_VERSION:0:1}-soft.tar.gz
+  DECOMPRESSED_KYLIN_PACKAGE=apache-kylin-${KYLIN_VERSION}-bin-spark${SPARK_VERSION:0:1}-soft
+
+  # Prepared the local cache dir for local cache + soft affinity
+
+  if [[ ! -d ${LOCAL_CACHE_DIR} ]]; then
+    sudo mkdir -p ${LOCAL_CACHE_DIR}
+    sudo chmod -R 777 ${LOCAL_CACHE_DIR}
+  fi
+
+  if [[ ! -d ${LOCAL_CACHE_DIR}/alluxio-cache-driver ]]; then
+    sudo mkdir -p ${LOCAL_CACHE_DIR}/alluxio-cache-driver
+    sudo chmod -R 777 ${LOCAL_CACHE_DIR}/alluxio-cache-driver
+  fi
+
+fi
+SPARK_PACKAGE=spark-${SPARK_VERSION}-bin-hadoop${HADOOP_VERSION:0:3}.tgz
+HADOOP_PACKAGE=hadoop-${HADOOP_VERSION}.tar.gz
+HIVE_PACKAGE=apache-hive-${HIVE_VERSION}-bin.tar.gz
+NODE_EXPORTER_PACKAGE=node_exporter-1.3.1.linux-amd64.tar.gz
+
+### Parameter for JDK 1.8
+JDK_PACKAGE=jdk-8u301-linux-x64.tar.gz
+JDK_DECOMPRESS_NAME=jdk1.8.0_301
+
+HOME_DIR=/home/ec2-user
+
+function init_env() {
+  HADOOP_DIR=${HOME_DIR}/hadoop
+  if [[ ! -d $HADOOP_DIR ]]; then
+    mkdir ${HADOOP_DIR}
+  fi
+
+  JAVA_HOME=/usr/local/java
+  JRE_HOME=${JAVA_HOME}/jre
+  HADOOP_HOME=${HADOOP_DIR}/hadoop-${HADOOP_VERSION}
+  HIVE_HOME=${HADOOP_DIR}/hive
+
+  KYLIN_HOME=${HOME_DIR}/${DECOMPRESSED_KYLIN_PACKAGE}
+  SPARK_HOME=${HADOOP_DIR}/spark
+  OUT_LOG=${HOME_DIR}/shell.stdout
+
+  # extra prometheus env
+  NODE_EXPORTER_HOME=/home/ec2-user/node_exporter
+
+  cat <<EOF >>~/.bash_profile
+## Set env variables
+### jdk env
+export JAVA_HOME=${JAVA_HOME}
+export JRE_HOME=${JRE_HOME}
+export CLASSPATH=.:${JAVA_HOME}/lib:${JRE_HOME}/lib
+
+#### hadoop env
+export HADOOP_HOME=${HADOOP_HOME}
+
+### hive env
+export HIVE_HOME=${HIVE_HOME}
+
+### prometheus related env
+export NODE_EXPORTER_HOME=${NODE_EXPORTER_HOME}
+
+### export all path
+export PATH=$HIVE_HOME/bin:$HIVE_HOME/conf:${HADOOP_HOME}/bin:${JAVA_HOME}/bin:$PATH
+
+### other
+export HOME_DIR=${HOME_DIR}
+export KYLIN_HOME=${KYLIN_HOME}
+export SPARK_HOME=${SPARK_HOME}
+export OUT_LOG=${OUT_LOG}
+EOF
+}
+
+if [[ ! -f ~/.inited_env ]]; then
+  logging info "Env variables not init, init it first ..."
+  init_env
+  touch ~/.inited_env
+else
+  logging warn "Env variables already inited, source it ..."
+fi
+# Init env variables
+source ~/.bash_profile
+
+exec 2>>${OUT_LOG}
+set -o pipefail
+# ================ Main Functions ======================
+PATH_TO_BUCKET=s3:/${BUCKET_SUFFIX}
+CONFIG_PATH_TO_BUCKET=s3a:/${BUCKET_SUFFIX}
+
+# Main Functions and Steps
+## prepare jdk env
+function prepare_jdk() {
+  logging info "Preparing Jdk ..."
+  if [[ -f ${HOME_DIR}/.prepared_jdk ]]; then
+    logging warn "Jdk already prepared, enjoy it."
+    return
+  fi
+
+  # copy jdk from s3 bucket to ec2 instances, so user need to upload jdk package first
+  aws s3 cp ${PATH_TO_BUCKET}/tar/${JDK_PACKAGE} ${HOME_DIR} --region ${CURRENT_REGION}
+  # unzip jdk: tar -C /extract/to/path -xzvf /path/to/archive.tar.gz
+  tar -zxf ${JDK_PACKAGE}
+  sudo mv ${JDK_DECOMPRESS_NAME} ${JAVA_HOME}
+  if [[ $? -ne 0 ]]; then
+    logging error "Java package was not installed well, pleas check ..."
+    exit 0
+  fi
+  logging info "Jdk inited ..."
+  touch ${HOME_DIR}/.prepared_jdk
+  logging info "Jdk is ready ..."
+}
+
+function init_jdk() {
+  if [[ -f ${HOME_DIR}/.inited_jdk ]]; then
+    logging warn "Jdk already inited, skip init ..."
+    return
+  fi
+  # this function is remove the unsupport tls rules in java which version of 1.8.291 and above
+  ## backup
+  cp -f $JAVA_HOME/jre/lib/security/java.security $JAVA_HOME/jre/lib/security/java.security.bak
+
+  ## modify the java.security file
+  sed -e "s/\ TLSv1,\ TLSv1.1,//g" -i $JAVA_HOME/jre/lib/security/java.security
+  logging info "Jdk inited ..."
+  touch ${HOME_DIR}/.inited_jdk
+}
+
+function prepare_hadoop() {
+  if [[ -f ${HOME_DIR}/.prepared_hadoop ]]; then
+    logging warn "Hadoop already prepared, skip init ..."
+    return
+  fi
+
+  if [[ -f ${HOME_DIR}/${HADOOP_PACKAGE} ]]; then
+    logging warn "Hadoop package ${HADOOP_PACKAGE} already downloaded, skip download ..."
+  else
+    logging info "Downloading Hadoop package ${HADOOP_PACKAGE} ..."
+    aws s3 cp ${PATH_TO_BUCKET}/tar/${HADOOP_PACKAGE} ${HOME_DIR} --region ${CURRENT_REGION}
+    #      # wget cost lot time
+    #      wget https://archive.apache.org/dist/hadoop/common/hadoop-${HADOOP_VERSION}/${HADOOP_PACKAGE}
+  fi
+
+  if [[ -d ${HOME_DIR}/hadoop-${HADOOP_VERSION} ]]; then
+    logging warn "Hadoop Package decompressed, skip decompress ..."
+  else
+    logging info "Decompress Hadoop package ..."
+    tar -zxf ${HADOOP_PACKAGE}
+  fi
+
+  logging info "Hadoop prepared ..."
+  touch ${HOME_DIR}/.prepared_hadoop
+}
+
+function init_hadoop() {
+  # match correct region endpoints for hadoop's core-site.xml
+  if [[ ${CURRENT_REGION} == "cn-northwest-1" ]]; then
+    S3_ENDPOINT=s3.cn-northwest-1.amazonaws.com.cn
+  elif [[ ${CURRENT_REGION} == "cn-north-1" ]]; then
+    S3_ENDPOINT=s3.cn-north-1.amazonaws.com.cn
+  else
+    S3_ENDPOINT=s3.${CURRENT_REGION}.amazonaws.com
+  fi
+
+  if [[ -f ${HOME_DIR}/.inited_hadoop ]]; then
+    logging warn "Hadoop already inited, skip init ..."
+  else
+    logging info "Init hadoop config ..."
+    # replace jars for hadoop, although it don't need to start
+    cp hadoop-${HADOOP_VERSION}/share/hadoop/tools/lib/aws-java-sdk-bundle-1.11.375.jar hadoop-${HADOOP_VERSION}/share/hadoop/common/lib/
+    cp hadoop-${HADOOP_VERSION}/share/hadoop/tools/lib/hadoop-aws-${HADOOP_VERSION}.jar hadoop-${HADOOP_VERSION}/share/hadoop/common/lib/
+
+    cat <<EOF >${HOME_DIR}/hadoop-${HADOOP_VERSION}/etc/hadoop/core-site.xml
+<?xml version="1.0" encoding="UTF-8"?>
+<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
+<configuration>
+	<property>
+	    <name>fs.default.name</name>
+	    <value>${CONFIG_PATH_TO_BUCKET}/working_dir</value>
+	</property>
+  <property>
+    <name>fs.s3a.endpoint</name>
+    <value>${S3_ENDPOINT}</value>
+  </property>
+</configuration>
+EOF
+
+    logging info "Moving hadoop package to ${HADOOP_HOME} ..."
+    mv ${HOME_DIR}/hadoop-${HADOOP_VERSION} ${HADOOP_HOME}
+
+    logging warn "touch ${HOME_DIR}/.inited_hadoop ... "
+    touch ${HOME_DIR}/.inited_hadoop
+  fi
+
+  logging info "Hadoop is ready ..."
+}
+
+function prepare_hive() {
+  logging info "Preparing hive ..."
+  if [[ -f ${HOME_DIR}/.prepared_hive ]]; then
+    logging warn "Hive already prepared, enjoy it."
+    return
+  fi
+
+  if [[ -f ${HOME_DIR}/${HIVE_PACKAGE} ]]; then
+    logging warn "${HIVE_PACKAGE} already downloaded, skip download it ..."
+  else
+    logging info "Downloading ${HIVE_PACKAGE} ..."
+    aws s3 cp ${PATH_TO_BUCKET}/tar/${HIVE_PACKAGE} ${HOME_DIR} --region ${CURRENT_REGION}
+    #      # wget cost lot time
+    #      wget https://downloads.apache.org/hive/hive-${HIVE_VERSION}/${HIVE_PACKAGE}
+  fi
+
+  if [[ -d ${HOME_DIR}/apache-hive-${HIVE_VERSION}-bin ]]; then
+    logging warn "Hive Package already decompressed, skip decompress ..."
+  else
+    logging info "Decompress Hive package ..."
+    tar -zxf ${HIVE_PACKAGE}
+  fi
+
+  if [[ -d ${HIVE_HOME} ]]; then
+    logging warn "Hive package already exists, skip moving ..."
+  else
+    logging info "Moving hive package ${HIVE_PACKAGE%*.*.*} to ${HIVE_HOME} ..."
+    mv ${HIVE_PACKAGE%*.*.*} ${HIVE_HOME}
+    if [[ $? -ne 0 ]]; then
+      logging error " Moving hive package failed, please check ..."
+      exit 0
+    fi
+  fi
+
+  # execute command
+  hive --version
+  if [[ $? -eq 0 ]]; then
+    logging info "Hive ${HIVE_VERSION} is ready ..."
+  else
+    logging error "Hive not prepared well, please check ..."
+    exit 0
+  fi
+
+  logging info "Hive prepared successfully ..."
+  touch ${HOME_DIR}/.prepared_hive
+}
+
+function init_hive() {
+  if [[ -f ${HOME_DIR}/.inited_hive ]]; then
+    logging warn "Hive already init, skip init ..."
+    return
+  fi
+
+  cat <<EOF >${HIVE_HOME}/conf/hive-site.xml
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
+<configuration>
+  <property>
+    <name>javax.jdo.option.ConnectionPassword</name>
+    <value>${DATABASE_PASSWORD}</value>
+    <description>password to use against metastore database</description>
+  </property>
+  <property>
+    <name>javax.jdo.option.ConnectionURL</name>
+    <value>jdbc:mysql://${DATABASE_HOST}:${DATABASE_PORT}/hive?createDatabaseIfNotExist=true&amp;useSSL=false</value>
+    <description>JDBC connect string for a JDBC metastore</description>
+  </property>
+  <property>
+    <name>javax.jdo.option.ConnectionDriverName</name>
+    <value>com.mysql.jdbc.Driver</value>
+    <description>Driver class name for a JDBC metastore</description>
+  </property>
+  <property>
+    <name>javax.jdo.option.ConnectionUserName</name>
+    <value>${DATABASE_USER}</value>
+    <description>Username to use against metastore database;default is root</description>
+  </property>
+  <property>
+    <name>hive.metastore.schema.verification</name>
+    <value>false</value>
+    <description>
+      Enforce metastore schema version consistency.
+      True: Verify that version information stored in metastore matches with one from Hive jars.  Also disable automatic
+            schema migration attempt. Users are required to manually migrate schema after Hive upgrade which ensures
+            proper metastore schema migration. (Default)
+      False: Warn if the version information stored in metastore doesn't match with one from in Hive jars.
+    </description>
+  </property>
+</configuration>
+EOF
+
+  # resolve jars conflict
+  if [[ ! -d $HIVE_HOME/spark_jar ]]; then
+    mkdir -p $HIVE_HOME/spark_jar
+    mv $HIVE_HOME/lib/spark-* $HIVE_HOME/spark_jar/
+    mv $HIVE_HOME/lib/jackson-module-scala_2.11-2.6.5.jar $HIVE_HOME/spark_jar/
+  fi
+
+  if [[ ! -f ${HIVE_HOME}/lib/mysql-connector-java-5.1.40.jar ]]; then
+    logging warn "${HIVE_HOME}/lib/mysql-connector-java-5.1.40.jar not exist, download it ..."
+    aws s3 cp ${PATH_TO_BUCKET}/jars/mysql-connector-java-5.1.40.jar ${HIVE_HOME}/lib/ --region ${CURRENT_REGION}
+  fi
+
+  logging warn "touch ${HOME_DIR}/.inited_hive ..."
+  # make a tag for hive inited
+  touch ${HOME_DIR}/.inited_hive
+  logging info "Hive is ready ..."
+}
+
+function prepare_spark() {
+  if [[ -f ${HOME_DIR}/.prepared_spark ]]; then
+    logging warn "Spark already prepared, enjoy it."
+    return
+  fi
+
+  logging info "Downloading Spark-${SPARK_VERSION} ..."
+  ## download spark
+  if [[ -f ${HOME_DIR}/${SPARK_PACKAGE} ]]; then
+    logging warn "${SPARK_PACKAGE} already download, skip download it."
+  else
+    logging warn "Downloading ${SPARK_PACKAGE} ..."
+    aws s3 cp ${PATH_TO_BUCKET}/tar/${SPARK_PACKAGE} ${HOME_DIR} --region ${CURRENT_REGION}
+    #      # wget cost lot time
+    #      wget http://archive.apache.org/dist/spark/spark-${SPARK_VERSION}/${SPARK_PACKAGE}
+  fi
+
+  if [[ -d ${HOME_DIR}/${SPARK_PACKAGE%*.*} ]]; then
+    logging warn "Spark package already decompressed, skip decompress ..."
+  else
+    logging info "Decompressing ${SPARK_PACKAGE} ..."
+    ### unzip spark tar file
+    tar -zxf ${SPARK_PACKAGE}
+  fi
+
+  if [[ -d ${SPARK_HOME} ]]; then
+    logging warn "${SPARK_HOME} already exists, skip moving ..."
+  else
+    logging info "Moving ${SPARK_PACKAGE%*.*} to ${SPARK_HOME}"
+    mv ${SPARK_PACKAGE%*.*} ${SPARK_HOME}
+  fi
+
+  logging info "Prepare Spark-${SPARK_VERSION} success."
+  touch ${HOME_DIR}/.prepared_spark
+}
+
+function init_spark() {
+  if [[ -f ${HOME_DIR}/.inited_spark ]]; then
+    logging warn "Spark already inited, enjoy it."
+    return
+  fi
+
+  # copy needed jars
+  if [[ ! -f $SPARK_HOME/jars/hadoop-aws-${HADOOP_VERSION}.jar ]]; then
+    cp $HADOOP_HOME/share/hadoop/tools/lib/hadoop-aws-${HADOOP_VERSION}.jar $SPARK_HOME/jars/
+  fi
+
+  if [[ ! -f $SPARK_HOME/jars/aws-java-sdk-bundle-1.11.375.jar ]]; then
+    cp $HADOOP_HOME/share/hadoop/tools/lib/aws-java-sdk-bundle-1.11.375.jar $SPARK_HOME/jars/
+  fi
+
+  if [[ ! -f $SPARK_HOME/jars/mysql-connector-java-5.1.40.jar ]]; then
+    cp $HIVE_HOME/lib/mysql-connector-java-5.1.40.jar $SPARK_HOME/jars/
+  fi
+
+  # hive-site.xml for spark
+  if [[ ! -f $SPARK_HOME/conf/hive-site.xml ]]; then
+    cp $HIVE_HOME/conf/hive-site.xml $SPARK_HOME/conf/
+  fi
+
+  #Support local cache + soft affinity
+  if [[ ${LOCAL_CACHE_SOFT_AFFINITY} == "true" ]]; then
+    if [[ ! -f $SPARK_HOME/jars/kylin-soft-affinity-cache-4.0.0-SNAPSHOT.jar ]]; then
+      logging info "Downloading kylin-soft-affinity-cache-4.0.0-SNAPSHOT.jar to $SPARK_HOME/jars/ ..."
+      aws s3 cp ${PATH_TO_BUCKET}/jars/kylin-soft-affinity-cache-4.0.0-SNAPSHOT.jar $SPARK_HOME/jars/ --region ${CURRENT_REGION}
+    fi
+
+    if [[ ! -f $SPARK_HOME/jars/alluxio-2.6.1-client.jar ]]; then
+      logging info "Downloading alluxio-2.6.1-client.jar to $SPARK_HOME/jars/ ..."
+      aws s3 cp ${PATH_TO_BUCKET}/jars/alluxio-2.6.1-client.jar $SPARK_HOME/jars/ --region ${CURRENT_REGION}
+    fi
+  fi
+
+  # Support prometheus metrics
+  cat <<EOF >${SPARK_HOME}/conf/metrics.properties
+*.sink.prometheusServlet.class=org.apache.spark.metrics.sink.PrometheusServlet
+*.sink.prometheusServlet.path=/metrics/prometheus
+master.sink.prometheusServlet.path=/metrics/master/prometheus
+applications.sink.prometheusServlet.path=/metrics/applications/prometheus
+EOF
+
+  logging info "Spark inited ..."
+  touch ${HOME_DIR}/.inited_spark
+  logging info "Spark is ready ..."
+}
+
+function prepare_kylin() {
+  logging info "Preparing kylin ..."
+
+  if [[ -f ${HOME_DIR}/.prepared_kylin ]]; then
+    logging warn "Kylin already prepared ..."
+    return
+  fi
+
+  if [[ -f ${HOME_DIR}/${KYLIN_PACKAGE} ]]; then
+    logging warn "Kylin package already downloaded, skip download it ..."
+  else
+    logging info "Kylin-${KYLIN_VERSION} downloading ..."
+    aws s3 cp ${PATH_TO_BUCKET}/tar/${KYLIN_PACKAGE} ${HOME_DIR} --region ${CURRENT_REGION}
+    #      # wget cost lot time
+    #      wget https://archive.apache.org/dist/kylin/apache-kylin-${KYLIN_VERSION}/${KYLIN_PACKAGE}
+  fi
+
+  if [[ -d ${HOME_DIR}/${DECOMPRESSED_KYLIN_PACKAGE} ]]; then
+    logging warn "Kylin package already decompress, skip decompress ..."
+  else
+    logging warn "Kylin package decompressing ..."
+    ### unzip kylin tar file
+    tar -zxf ${KYLIN_PACKAGE}
+  fi
+
+  logging info "Kylin inited ..."
+  touch ${HOME_DIR}/.prepared_kylin
+  logging info "Kylin prepared ..."
+}
+
+function init_kylin() {
+  if [[ -f ${HOME_DIR}/.inited_kylin ]]; then
+    logging warn "Kylin already inited ..."
+    return
+  fi
+
+  if [[ ! -d ${KYLIN_HOME}/ext ]]; then
+    ### create dir directory for other dependency used by kylin
+    mkdir -p ${KYLIN_HOME}/ext
+  fi
+
+  if [[ ! -f $KYLIN_HOME/ext/mysql-connector-java-5.1.40.jar ]]; then
+    aws s3 cp ${PATH_TO_BUCKET}/jars/mysql-connector-java-5.1.40.jar $KYLIN_HOME/ext/ --region ${CURRENT_REGION}
+  fi
+
+  if [[ ! -f $KYLIN_HOME/ext/slf4j-log4j12-1.7.25.jar ]]; then
+    cp $HADOOP_HOME/share/hadoop/common/lib/slf4j-log4j12-1.7.25.jar $KYLIN_HOME/ext/
+  fi
+
+  if [[ ! -f $KYLIN_HOME/ext/log4j-1.2.17.jar ]]; then
+    cp $HADOOP_HOME/share/hadoop/common/lib/log4j-1.2.17.jar $KYLIN_HOME/ext/
+  fi
+
+  #Support local cache + soft affinity
+  if [[ $LOCAL_CACHE_SOFT_AFFINITY == "true" ]]; then
+    if [[ ! -f $KYLIN_HOME/ext/kylin-soft-affinity-cache-4.0.0-SNAPSHOT.jar ]]; then
+      logging info "Downloading kylin-soft-affinity-cache-4.0.0-SNAPSHOT.jar to $KYLIN_HOME/ext/ ..."
+      aws s3 cp ${PATH_TO_BUCKET}/jars/kylin-soft-affinity-cache-4.0.0-SNAPSHOT.jar $KYLIN_HOME/ext/ --region ${CURRENT_REGION}
+    fi
+
+    if [[ ! -f $KYLIN_HOME/ext/alluxio-2.6.1-client.jar ]]; then
+      logging info "Downloading alluxio-2.6.1-client.jar to $KYLIN_HOME/ext/ ..."
+      aws s3 cp ${PATH_TO_BUCKET}/jars/alluxio-2.6.1-client.jar $KYLIN_HOME/ext/ --region ${CURRENT_REGION}
+    fi
+  fi
+
+  # Overwrite kylin.properties
+  logging info "Overwrite kylin.properties from ${PATH_TO_BUCKET}/properties/${CLUSTER_NUM}/kylin.properties to ${KYLIN_HOME}/conf/kylin.properties in region ${CURRENT_REGION}."
+  aws s3 cp ${PATH_TO_BUCKET}/properties/${CLUSTER_NUM}/kylin.properties ${KYLIN_HOME}/conf/kylin.properties --region ${CURRENT_REGION}
+
+  if [[ ${LOCAL_CACHE_SOFT_AFFINITY} == "true" ]]; then
+    cat <<EOF >> ${KYLIN_HOME}/conf/kylin.properties
+kylin.query.spark-conf.spark.executor.extraJavaOptions=-Dhdp.version=current -Dlog4j.configuration=spark-executor-log4j.properties -Dlog4j.debug -Dkylin.hdfs.working.dir=\${kylin.env.hdfs-working-dir} -Dkylin.metadata.identifier=\${kylin.metadata.url.identifier} -Dkylin.spark.category=sparder -Dkylin.spark.identifier={{APP_ID}} -Dalluxio.user.client.cache.dir=${LOCAL_CACHE_DIR}/alluxio-cache-{{APP_ID}}-{{EXECUTOR_ID}}
+
+kylin.query.spark-conf.spark.driver.extraJavaOptions=-Dhdp.version=current -Dalluxio.user.client.cache.dir=${LOCAL_CACHE_DIR}/alluxio-cache-driver
+
+kylin.query.spark-conf.spark.sql.sources.ignoreDataLocality=true
+kylin.query.spark-conf.spark.extraListeners=org.apache.kylin.softaffinity.scheduler.SoftAffinityListener
+kylin.query.spark-conf.spark.hadoop.spark.kylin.local-cache.enabled=true
+kylin.query.spark-conf.spark.kylin.soft-affinity.enabled=true
+kylin.query.spark-conf.spark.kylin.soft-affinity.replications.num=1
+kylin.query.spark-conf.spark.hadoop.io.file.buffer.size=524288
+kylin.query.spark-conf.spark.hadoop.fs.hdfs.impl=org.apache.kylin.cache.fs.kylin.KylinCacheFileSystem
+
+kylin.query.spark-conf.spark.hadoop.fs.s3.impl=org.apache.kylin.cache.fs.kylin.KylinCacheFileSystem
+kylin.query.spark-conf.spark.hadoop.fs.s3a.impl=org.apache.kylin.cache.fs.kylin.KylinCacheFileSystem
+
+kylin.query.spark-conf.spark.hadoop.fs.s3a.experimental.input.fadvise=random
+kylin.query.spark-conf.spark.hadoop.parquet.enable.summary-metadata=false
+kylin.query.spark-conf.spark.sql.parquet.mergeSchema=false
+kylin.query.spark-conf.spark.sql.hive.metastorePartitionPruning=true
+
+
+
+# LOCAL or BUFF
+kylin.query.spark-conf.spark.hadoop.alluxio.user.client.cache.store.type=LOCAL
+kylin.query.spark-conf.spark.hadoop.alluxio.user.client.cache.async.restore.enabled=true
+kylin.query.spark-conf.spark.hadoop.alluxio.user.client.cache.async.write.enabled=true
+kylin.query.spark-conf.spark.hadoop.alluxio.user.client.cache.async.write.threads=6
+kylin.query.spark-conf.spark.hadoop.alluxio.user.client.cache.size=20GB
+kylin.query.spark-conf.spark.hadoop.alluxio.user.client.cache.page.size=1MB
+kylin.query.spark-conf.spark.hadoop.alluxio.user.client.cache.local.store.file.buckets=1000
+kylin.query.spark-conf.spark.hadoop.alluxio.user.update.file.accesstime.disabled=true
+EOF
+  fi
+
+  logging info "Kylin inited ..."
+  touch ${HOME_DIR}/.inited_kylin
+  logging info "Kylin is ready ..."
+}
+
+function after_start_kylin() {
+  KYLIN_WEB_LIB_PATH=$KYLIN_HOME/tomcat/webapps/kylin/WEB-INF/lib
+  if [[ ! -f $KYLIN_WEB_LIB_PATH/commons-collections-3.2.2.jar ]]; then
+    cp ${HIVE_HOME}/lib/commons-collections-3.2.2.jar $KYLIN_WEB_LIB_PATH/
+  fi
+
+  if [[ ! -f $KYLIN_WEB_LIB_PATH/commons-configuration-1.3.jar ]]; then
+    aws s3 cp ${PATH_TO_BUCKET}/jars/commons-configuration-1.3.jar $KYLIN_WEB_LIB_PATH/ --region ${CURRENT_REGION}
+  fi
+
+  if [[ ! -f $KYLIN_WEB_LIB_PATH/aws-java-sdk-bundle-1.11.375.jar ]]; then
+    cp $HADOOP_HOME/share/hadoop/common/lib/aws-java-sdk-bundle-1.11.375.jar $KYLIN_WEB_LIB_PATH/
+  fi
+
+  if [[ ! -f $KYLIN_WEB_LIB_PATH/hadoop-aws-3.2.0.jar ]]; then
+    cp $HADOOP_HOME/share/hadoop/common/lib/hadoop-aws-3.2.0.jar $KYLIN_WEB_LIB_PATH/
+  fi
+}
+
+function start_kylin() {
+  ${KYLIN_HOME}/bin/kylin.sh start
+  sleep 30
+}
+
+function sample_for_kylin() {
+  if [[ ${IS_SCALED} == 'false' ]]; then
+      ${KYLIN_HOME}/bin/sample.sh
+      if [[ $? -ne 0 ]]; then
+        logging error "Sample for kylin is failed, please check ..."
+      else
+        logging info "Sample for kylin is successful, enjoy it ..."
+      fi
+  else
+    logging info "It is unnecessary to sample data in scaled mode. "
+  fi
+}
+
+function restart_kylin() {
+  ${KYLIN_HOME}/bin/kylin.sh restart
+}
+
+function prepare_node_exporter() {
+  logging info "Preparing node_exporter ..."
+  if [[ -f ${HOME_DIR}/.prepared_node_exporter ]]; then
+    logging warn "NODE_EXPORTER already prepared, skip prepare ... "
+    return
+  fi
+
+  if [[ ! -f ${HOME_DIR}/${NODE_EXPORTER_PACKAGE} ]]; then
+    logging info "NODE_EXPORTER package ${NODE_EXPORTER_PACKAGE} not downloaded, downloading it ..."
+    aws s3 cp ${PATH_TO_BUCKET}/tar/${NODE_EXPORTER_PACKAGE} ${HOME_DIR} --region ${CURRENT_REGION}
+  else
+    logging warn "NODE_EXPORTER package ${NODE_EXPORTER_PACKAGE} already download, skip download it."
+  fi
+  touch ${HOME_DIR}/.prepared_node_exporter
+  logging info "NODE_EXPORTER prepared ..."
+}
+
+function init_node_exporter() {
+  logging info "Initializing node_exporter ..."
+  if [[ -f ${HOME_DIR}/.inited_node_exporter ]]; then
+    logging warn "NODE_EXPORTER already inited, skip init ... "
+    return
+  fi
+
+  if [[ ! -f ${NODE_EXPORTER_HOME} ]]; then
+    logging info "NODE_EXPORTER home ${NODE_EXPORTER_HOME} not ready, decompressing ${NODE_EXPORTER_PACKAGE} ..."
+    tar -zxf ${HOME_DIR}/${NODE_EXPORTER_PACKAGE}
+    mv ${HOME_DIR}/${NODE_EXPORTER_PACKAGE%.tar.gz} ${NODE_EXPORTER_HOME}
+  else
+    logging warn "NODE_EXPORTER home ${PROMETHEUS_PACKAGE} already ready."
+  fi
+  touch ${HOME_DIR}/.inited_node_exporter
+  logging info "NODE_EXPORTER inited ..."
+}
+
+function start_node_exporter() {
+  # NOTE: default node_exporter port 9100
+  logging info "Start node_exporter ..."
+  nohup ${NODE_EXPORTER_HOME}/node_exporter >>${NODE_EXPORTER_HOME}/node.log 2>&1 &
+}
+
+function prepare_packages() {
+  if [[ -f ${HOME_DIR}/.prepared_packages ]]; then
+    logging warn "Packages already prepared, skip prepare ..."
+    return
+  fi
+
+  prepare_jdk
+  init_jdk
+
+  # add extra monitor service
+  prepare_node_exporter
+  init_node_exporter
+  # start node_exporter quickly
+  start_node_exporter
+
+  prepare_hadoop
+  init_hadoop
+
+  prepare_hive
+  init_hive
+
+  prepare_spark
+  init_spark
+
+  prepare_kylin
+  init_kylin
+
+  touch ${HOME_DIR}/.prepared_packages
+  logging info "All need packages are ready ..."
+}
+
+function start_services_on_kylin() {
+  # special step for compatible jars, details in after_start_kylin
+  sample_for_kylin
+  start_kylin
+  after_start_kylin
+  restart_kylin
+}
+
+function main() {
+  prepare_packages
+  start_services_on_kylin
+}
+
+main
diff --git a/backup/scripts/prepare-ec2-env-for-spark-master.sh b/backup/scripts/prepare-ec2-env-for-spark-master.sh
new file mode 100644
index 0000000..496dab9
--- /dev/null
+++ b/backup/scripts/prepare-ec2-env-for-spark-master.sh
@@ -0,0 +1,587 @@
+#!/bin/bash
+
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# Note: this script is for AWS ec2 instances
+set -e
+
+# ============= Utils function ============
+function info() {
+  # shellcheck disable=SC2145
+  echo -e "\033[32m$@\033[0m"
+}
+
+function warn() {
+  # shellcheck disable=SC2145
+  echo -e "\033[33m$@\033[0m"
+}
+
+function error() {
+  # shellcheck disable=SC2145
+  echo -e "\033[31m$@\033[0m"
+}
+
+function logging() {
+  case $1 in
+  "info")
+    shift
+    # shellcheck disable=SC2068
+    info $@
+    ;;
+  "warn")
+    shift
+    # shellcheck disable=SC2068
+    warn $@
+    ;;
+  "error")
+    shift
+    # shellcheck disable=SC2068
+    error $@
+    ;;
+  *)
+    # shellcheck disable=SC2068
+    echo -e $@
+    ;;
+  esac
+}
+
+set +e
+
+function help() {
+  logging warn "Invalid input."
+  logging warn "Usage: ${BASH_SOURCE[0]}
+                    --bucket-url /path/to/bucket/without/prefix
+                    --region region-for-s3
+                    --db-host db-host-for-hive-metadata
+                    --db-password db-password-for-hive-metadata
+                    --db-user db-user-for-hive-metadata
+                    --db-port db-port-for-hive-metadata
+                    --local-soft whether-to-use-local-cache+soft-affinity"
+  exit 0
+}
+
+if [[ $# -ne 14 ]]; then
+  help
+fi
+
+while [[ $# != 0 ]]; do
+  if [[ $1 == "--bucket-url" ]]; then
+    # url same as: /xxx/kylin
+    BUCKET_SUFFIX=$2
+  elif [[ $1 == "--region" ]]; then
+    CURRENT_REGION=$2
+  elif [[ $1 == "--db-host" ]]; then
+    DATABASE_HOST=$2
+  elif [[ $1 == "--db-password" ]]; then
+    DATABASE_PASSWORD=$2
+  elif [[ $1 == "--db-user" ]]; then
+    DATABASE_USER=$2
+  elif [[ $1 == "--db-port" ]]; then
+    DATABASE_PORT=$2
+  elif [[ $1 == "--local-soft" ]]; then
+    LOCAL_CACHE_SOFT_AFFINITY=$2
+  else
+    help
+  fi
+  shift
+  shift
+done
+
+# =============== Env Parameters =================
+# Prepare Steps
+### Parameters for Spark and Kylin
+#### ${SPARK_VERSION:0:1} get 2 from 2.4.7
+HADOOP_VERSION=3.2.0
+SPARK_VERSION=3.1.1
+HIVE_VERSION=2.3.9
+
+LOCAL_CACHE_DIR=/home/ec2-user/ssd
+
+### File name
+if [[ $LOCAL_CACHE_SOFT_AFFINITY == "true" ]]; then
+  # Prepared the local cache dir for local cache + soft affinity
+  if [[ ! -d ${LOCAL_CACHE_DIR} ]]; then
+      sudo mkdir -p ${LOCAL_CACHE_DIR}
+      sudo chmod -R 777 ${LOCAL_CACHE_DIR}
+  fi
+
+  if [[ ! -d ${LOCAL_CACHE_DIR}/alluxio-cache-driver ]]; then
+      sudo mkdir -p ${LOCAL_CACHE_DIR}/alluxio-cache-driver
+      sudo chmod -R 777 ${LOCAL_CACHE_DIR}/alluxio-cache-driver
+  fi
+
+fi
+SPARK_PACKAGE=spark-${SPARK_VERSION}-bin-hadoop${HADOOP_VERSION:0:3}.tgz
+HADOOP_PACKAGE=hadoop-${HADOOP_VERSION}.tar.gz
+HIVE_PACKAGE=apache-hive-${HIVE_VERSION}-bin.tar.gz
+NODE_EXPORTER_PACKAGE=node_exporter-1.3.1.linux-amd64.tar.gz
+
+### Parameter for JDK 1.8
+JDK_PACKAGE=jdk-8u301-linux-x64.tar.gz
+JDK_DECOMPRESS_NAME=jdk1.8.0_301
+
+HOME_DIR=/home/ec2-user
+
+function init_env() {
+  HADOOP_DIR=${HOME_DIR}/hadoop
+  if [[ ! -d $HADOOP_DIR ]]; then
+    mkdir ${HADOOP_DIR}
+  fi
+
+  JAVA_HOME=/usr/local/java
+  JRE_HOME=${JAVA_HOME}/jre
+  HADOOP_HOME=${HADOOP_DIR}/hadoop-${HADOOP_VERSION}
+  HIVE_HOME=${HADOOP_DIR}/hive
+
+  SPARK_HOME=${HADOOP_DIR}/spark
+  OUT_LOG=${HOME_DIR}/shell.stdout
+
+  # extra prometheus env
+  NODE_EXPORTER_HOME=/home/ec2-user/node_exporter
+
+  cat <<EOF >>~/.bash_profile
+## Set env variables
+### jdk env
+export JAVA_HOME=${JAVA_HOME}
+export JRE_HOME=${JRE_HOME}
+export CLASSPATH=.:${JAVA_HOME}/lib:${JRE_HOME}/lib
+
+#### hadoop env
+export HADOOP_HOME=${HADOOP_HOME}
+
+### hive env
+export HIVE_HOME=${HIVE_HOME}
+
+### prometheus related env
+export NODE_EXPORTER_HOME=${NODE_EXPORTER_HOME}
+
+### export all path
+export PATH=$HIVE_HOME/bin:$HIVE_HOME/conf:${HADOOP_HOME}/bin:${JAVA_HOME}/bin:$PATH
+
+### other
+export HOME_DIR=${HOME_DIR}
+export SPARK_HOME=${SPARK_HOME}
+export OUT_LOG=${OUT_LOG}
+EOF
+}
+
+if [[ ! -f ~/.inited_env ]]; then
+  logging info "Env variables not init, init it first ..."
+  init_env
+  touch ~/.inited_env
+else
+  logging warn "Env variables already inited, source it ..."
+fi
+# Init env variables
+source ~/.bash_profile
+
+exec 2>>${OUT_LOG}
+set -o pipefail
+# ================ Main Functions ======================
+PATH_TO_BUCKET=s3:/${BUCKET_SUFFIX}
+CONFIG_PATH_TO_BUCKET=s3a:/${BUCKET_SUFFIX}
+
+# Main Functions and Steps
+## prepare jdk env
+function prepare_jdk() {
+  logging info "Preparing Jdk ..."
+  if [[ -f ${HOME_DIR}/.prepared_jdk ]]; then
+    logging warn "Jdk already prepared, enjoy it."
+    return
+  fi
+
+  # copy jdk from s3 bucket to ec2 instances, so user need to upload jdk package first
+  aws s3 cp ${PATH_TO_BUCKET}/tar/${JDK_PACKAGE} ${HOME_DIR} --region ${CURRENT_REGION}
+  # unzip jdk: tar -C /extract/to/path -xzvf /path/to/archive.tar.gz
+  tar -zxf ${JDK_PACKAGE}
+  sudo mv ${JDK_DECOMPRESS_NAME} ${JAVA_HOME}
+  if [[ $? -ne 0 ]]; then
+    logging error "Java package was not installed well, pleas check ..."
+    exit 0
+  fi
+  logging info "Jdk inited ..."
+  touch ${HOME_DIR}/.prepared_jdk
+  logging info "Jdk is ready ..."
+}
+
+function init_jdk() {
+  if [[ -f ${HOME_DIR}/.inited_jdk ]]; then
+    logging warn "Jdk already inited, skip init ..."
+    return
+  fi
+  # this function is remove the unsupport tls rules in java which version of 1.8.291 and above
+  ## backup
+  cp -f $JAVA_HOME/jre/lib/security/java.security $JAVA_HOME/jre/lib/security/java.security.bak
+
+  ## modify the java.security file
+  sed -e "s/\ TLSv1,\ TLSv1.1,//g" -i $JAVA_HOME/jre/lib/security/java.security
+  logging info "Jdk inited ..."
+  touch ${HOME_DIR}/.inited_jdk
+}
+
+function prepare_hadoop() {
+  if [[ -f ${HOME_DIR}/.prepared_hadoop ]]; then
+    logging warn "Hadoop already prepared, skip init ..."
+    return
+  fi
+
+  if [[ -f ${HOME_DIR}/${HADOOP_PACKAGE} ]]; then
+    logging warn "Hadoop package ${HADOOP_PACKAGE} already downloaded, skip download ..."
+  else
+    logging info "Downloading Hadoop package ${HADOOP_PACKAGE} ..."
+    aws s3 cp ${PATH_TO_BUCKET}/tar/${HADOOP_PACKAGE} ${HOME_DIR} --region ${CURRENT_REGION}
+    #      # wget cost lot time
+    #      wget https://archive.apache.org/dist/hadoop/common/hadoop-${HADOOP_VERSION}/${HADOOP_PACKAGE}
+  fi
+
+  if [[ -d ${HOME_DIR}/hadoop-${HADOOP_VERSION} ]]; then
+    logging warn "Hadoop Package decompressed, skip decompress ..."
+  else
+    logging info "Decompress Hadoop package ..."
+    tar -zxf ${HADOOP_PACKAGE}
+  fi
+
+  logging info "Hadoop prepared ..."
+  touch ${HOME_DIR}/.prepared_hadoop
+}
+
+function init_hadoop() {
+  # match correct region endpoints for hadoop's core-site.xml
+  if [[ ${CURRENT_REGION} == "cn-northwest-1" ]]; then
+    S3_ENDPOINT=s3.cn-northwest-1.amazonaws.com.cn
+  elif [[ ${CURRENT_REGION} == "cn-north-1" ]]; then
+    S3_ENDPOINT=s3.cn-north-1.amazonaws.com.cn
+  else
+    S3_ENDPOINT=s3.${CURRENT_REGION}.amazonaws.com
+  fi
+
+  if [[ -f ${HOME_DIR}/.inited_hadoop ]]; then
+    logging warn "Hadoop already inited, skip init ..."
+  else
+    logging info "Init hadoop config ..."
+    # replace jars for hadoop, although it don't need to start
+    cp hadoop-${HADOOP_VERSION}/share/hadoop/tools/lib/aws-java-sdk-bundle-1.11.375.jar hadoop-${HADOOP_VERSION}/share/hadoop/common/lib/
+    cp hadoop-${HADOOP_VERSION}/share/hadoop/tools/lib/hadoop-aws-${HADOOP_VERSION}.jar hadoop-${HADOOP_VERSION}/share/hadoop/common/lib/
+
+    cat <<EOF >${HOME_DIR}/hadoop-${HADOOP_VERSION}/etc/hadoop/core-site.xml
+<?xml version="1.0" encoding="UTF-8"?>
+<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
+<configuration>
+	<property>
+	    <name>fs.default.name</name>
+	    <value>${CONFIG_PATH_TO_BUCKET}/working_dir</value>
+	</property>
+  <property>
+    <name>fs.s3a.endpoint</name>
+    <value>${S3_ENDPOINT}</value>
+  </property>
+</configuration>
+EOF
+
+    logging info "Moving hadoop package to ${HADOOP_HOME} ..."
+    mv ${HOME_DIR}/hadoop-${HADOOP_VERSION} ${HADOOP_HOME}
+
+    logging warn "touch ${HOME_DIR}/.inited_hadoop ... "
+    touch ${HOME_DIR}/.inited_hadoop
+  fi
+
+  logging info "Hadoop is ready ..."
+}
+
+function prepare_hive() {
+  logging info "Preparing hive ..."
+  if [[ -f ${HOME_DIR}/.prepared_hive ]]; then
+    logging warn "Hive already prepared, enjoy it."
+    return
+  fi
+
+  if [[ -f ${HOME_DIR}/${HIVE_PACKAGE} ]]; then
+    logging warn "${HIVE_PACKAGE} already downloaded, skip download it ..."
+  else
+    logging info "Downloading ${HIVE_PACKAGE} ..."
+    aws s3 cp ${PATH_TO_BUCKET}/tar/${HIVE_PACKAGE} ${HOME_DIR} --region ${CURRENT_REGION}
+    #      # wget cost lot time
+    #      wget https://downloads.apache.org/hive/hive-${HIVE_VERSION}/${HIVE_PACKAGE}
+  fi
+
+  if [[ -d ${HOME_DIR}/apache-hive-${HIVE_VERSION}-bin ]]; then
+    logging warn "Hive Package already decompressed, skip decompress ..."
+  else
+    logging info "Decompress Hive package ..."
+    tar -zxf ${HIVE_PACKAGE}
+  fi
+
+  if [[ -d ${HIVE_HOME} ]]; then
+    logging warn "Hive package already exists, skip moving ..."
+  else
+    logging info "Moving hive package ${HIVE_PACKAGE%*.*.*} to ${HIVE_HOME} ..."
+    mv ${HIVE_PACKAGE%*.*.*} ${HIVE_HOME}
+    if [[ $? -ne 0 ]]; then
+      logging error " Moving hive package failed, please check ..."
+      exit 0
+    fi
+  fi
+
+  # execute command
+  hive --version
+  if [[ $? -eq 0 ]]; then
+    logging info "Hive ${HIVE_VERSION} is ready ..."
+  else
+    logging error "Hive not prepared well, please check ..."
+    exit 0
+  fi
+
+  logging info "Hive prepared successfully ..."
+  touch ${HOME_DIR}/.prepared_hive
+}
+
+function init_hive() {
+  if [[ -f ${HOME_DIR}/.inited_hive ]]; then
+    logging warn "Hive already init, skip init ..."
+    return
+  fi
+
+  cat <<EOF >${HIVE_HOME}/conf/hive-site.xml
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
+<configuration>
+  <property>
+    <name>javax.jdo.option.ConnectionPassword</name>
+    <value>${DATABASE_PASSWORD}</value>
+    <description>password to use against metastore database</description>
+  </property>
+  <property>
+    <name>javax.jdo.option.ConnectionURL</name>
+    <value>jdbc:mysql://${DATABASE_HOST}:${DATABASE_PORT}/hive?createDatabaseIfNotExist=true&amp;useSSL=false</value>
+    <description>JDBC connect string for a JDBC metastore</description>
+  </property>
+  <property>
+    <name>javax.jdo.option.ConnectionDriverName</name>
+    <value>com.mysql.jdbc.Driver</value>
+    <description>Driver class name for a JDBC metastore</description>
+  </property>
+  <property>
+    <name>javax.jdo.option.ConnectionUserName</name>
+    <value>${DATABASE_USER}</value>
+    <description>Username to use against metastore database;default is root</description>
+  </property>
+  <property>
+    <name>hive.metastore.schema.verification</name>
+    <value>false</value>
+    <description>
+      Enforce metastore schema version consistency.
+      True: Verify that version information stored in metastore matches with one from Hive jars.  Also disable automatic
+            schema migration attempt. Users are required to manually migrate schema after Hive upgrade which ensures
+            proper metastore schema migration. (Default)
+      False: Warn if the version information stored in metastore doesn't match with one from in Hive jars.
+    </description>
+  </property>
+</configuration>
+EOF
+
+  # resolve jars conflict
+  if [[ ! -d $HIVE_HOME/spark_jar ]]; then
+    mkdir -p $HIVE_HOME/spark_jar
+    mv $HIVE_HOME/lib/spark-* $HIVE_HOME/spark_jar/
+    mv $HIVE_HOME/lib/jackson-module-scala_2.11-2.6.5.jar $HIVE_HOME/spark_jar/
+  fi
+
+  if [[ ! -f ${HIVE_HOME}/lib/mysql-connector-java-5.1.40.jar ]]; then
+    logging warn "${HIVE_HOME}/lib/mysql-connector-java-5.1.40.jar not exist, download it ..."
+    aws s3 cp ${PATH_TO_BUCKET}/jars/mysql-connector-java-5.1.40.jar ${HIVE_HOME}/lib/ --region ${CURRENT_REGION}
+  fi
+
+  logging warn "touch ${HOME_DIR}/.inited_hive ..."
+  # make a tag for hive inited
+  touch ${HOME_DIR}/.inited_hive
+  logging info "Hive is ready ..."
+}
+
+function prepare_spark() {
+  if [[ -f ${HOME_DIR}/.prepared_spark ]]; then
+    logging warn "Spark already prepared, enjoy it."
+    return
+  fi
+
+  logging info "Downloading Spark-${SPARK_VERSION} ..."
+  ## download spark
+  if [[ -f ${HOME_DIR}/${SPARK_PACKAGE} ]]; then
+    logging warn "${SPARK_PACKAGE} already download, skip download it."
+  else
+    logging warn "Downloading ${SPARK_PACKAGE} ..."
+    aws s3 cp ${PATH_TO_BUCKET}/tar/${SPARK_PACKAGE} ${HOME_DIR} --region ${CURRENT_REGION}
+    #      # wget cost lot time
+    #      wget http://archive.apache.org/dist/spark/spark-${SPARK_VERSION}/${SPARK_PACKAGE}
+  fi
+
+  if [[ -d ${HOME_DIR}/${SPARK_PACKAGE%*.*} ]]; then
+    logging warn "Spark package already decompressed, skip decompress ..."
+  else
+    logging info "Decompressing ${SPARK_PACKAGE} ..."
+    ### unzip spark tar file
+    tar -zxf ${SPARK_PACKAGE}
+  fi
+
+  if [[ -d ${SPARK_HOME} ]]; then
+    logging warn "${SPARK_HOME} already exists, skip moving ..."
+  else
+    logging info "Moving ${SPARK_PACKAGE%*.*} to ${SPARK_HOME}"
+    mv ${SPARK_PACKAGE%*.*} ${SPARK_HOME}
+  fi
+
+  logging info "Prepare Spark-${SPARK_VERSION} success."
+  touch ${HOME_DIR}/.prepared_spark
+}
+
+function init_spark() {
+  if [[ -f ${HOME_DIR}/.inited_spark ]]; then
+    logging warn "Spark already inited, enjoy it."
+    return
+  fi
+
+  # copy needed jars
+  if [[ ! -f $SPARK_HOME/jars/hadoop-aws-${HADOOP_VERSION}.jar ]]; then
+    cp $HADOOP_HOME/share/hadoop/tools/lib/hadoop-aws-${HADOOP_VERSION}.jar $SPARK_HOME/jars/
+  fi
+
+  if [[ ! -f $SPARK_HOME/jars/aws-java-sdk-bundle-1.11.375.jar ]]; then
+    cp $HADOOP_HOME/share/hadoop/tools/lib/aws-java-sdk-bundle-1.11.375.jar $SPARK_HOME/jars/
+  fi
+
+  if [[ ! -f $SPARK_HOME/jars/mysql-connector-java-5.1.40.jar ]]; then
+    cp $HIVE_HOME/lib/mysql-connector-java-5.1.40.jar $SPARK_HOME/jars/
+  fi
+
+  # hive-site.xml for spark
+  if [[ ! -f $SPARK_HOME/conf/hive-site.xml ]]; then
+    cp $HIVE_HOME/conf/hive-site.xml $SPARK_HOME/conf/
+  fi
+
+  #Support local cache + soft affinity
+  if [[ $LOCAL_CACHE_SOFT_AFFINITY == "true" ]]; then
+    if [[ ! -f $SPARK_HOME/jars/kylin-soft-affinity-cache-4.0.0-SNAPSHOT.jar ]]; then
+      logging info "Downloading kylin-soft-affinity-cache-4.0.0-SNAPSHOT.jar to $SPARK_HOME/jars/ ..."
+      aws s3 cp ${PATH_TO_BUCKET}/jars/kylin-soft-affinity-cache-4.0.0-SNAPSHOT.jar $SPARK_HOME/jars/ --region ${CURRENT_REGION}
+    fi
+
+    if [[ ! -f $SPARK_HOME/jars/alluxio-2.6.1-client.jar ]]; then
+      logging info "Downloading alluxio-2.6.1-client.jar to $SPARK_HOME/jars/ ..."
+      aws s3 cp ${PATH_TO_BUCKET}/jars/alluxio-2.6.1-client.jar $SPARK_HOME/jars/ --region ${CURRENT_REGION}
+    fi
+  fi
+
+  # Support prometheus metrics
+  cat <<EOF > ${SPARK_HOME}/conf/metrics.properties
+*.sink.prometheusServlet.class=org.apache.spark.metrics.sink.PrometheusServlet
+*.sink.prometheusServlet.path=/metrics/prometheus
+master.sink.prometheusServlet.path=/metrics/master/prometheus
+applications.sink.prometheusServlet.path=/metrics/applications/prometheus
+EOF
+
+  logging info "Spark inited ..."
+  touch ${HOME_DIR}/.inited_spark
+  logging info "Spark is ready ..."
+}
+
+function start_spark_master() {
+  $SPARK_HOME/sbin/start-master.sh
+  if [[ $? -ne 0 ]]; then
+    logging error "spark start master failed, please check ..."
+    exit 0
+  fi
+  logging info "Start Spark master successfully ..."
+}
+
+function prepare_node_exporter() {
+  logging info "Preparing node_exporter ..."
+  if [[ -f ${HOME_DIR}/.prepared_node_exporter ]]; then
+    logging warn "NODE_EXPORTER already prepared, skip prepare ... "
+    return
+  fi
+
+  if [[ ! -f ${HOME_DIR}/${NODE_EXPORTER_PACKAGE} ]]; then
+    logging info "NODE_EXPORTER package ${NODE_EXPORTER_PACKAGE} not downloaded, downloading it ..."
+    aws s3 cp ${PATH_TO_BUCKET}/tar/${NODE_EXPORTER_PACKAGE} ${HOME_DIR} --region ${CURRENT_REGION}
+  else
+    logging warn "NODE_EXPORTER package ${NODE_EXPORTER_PACKAGE} already download, skip download it."
+  fi
+  touch ${HOME_DIR}/.prepared_node_exporter
+  logging info "NODE_EXPORTER prepared ..."
+}
+
+function init_node_exporter() {
+  logging info "Initializing node_exporter ..."
+  if [[ -f ${HOME_DIR}/.inited_node_exporter ]]; then
+    logging warn "NODE_EXPORTER already inited, skip init ... "
+    return
+  fi
+
+  if [[ ! -f ${NODE_EXPORTER_HOME} ]]; then
+    logging info "NODE_EXPORTER home ${NODE_EXPORTER_HOME} not ready, decompressing ${NODE_EXPORTER_PACKAGE} ..."
+    tar -zxf ${HOME_DIR}/${NODE_EXPORTER_PACKAGE}
+    mv ${HOME_DIR}/${NODE_EXPORTER_PACKAGE%.tar.gz} ${NODE_EXPORTER_HOME}
+  else
+    logging warn "NODE_EXPORTER home ${PROMETHEUS_PACKAGE} already ready."
+  fi
+  touch ${HOME_DIR}/.inited_node_exporter
+  logging info "NODE_EXPORTER inited ..."
+}
+
+function start_node_exporter() {
+  # NOTE: default node_exporter port 9100
+  logging info "Start node_exporter ..."
+  nohup ${NODE_EXPORTER_HOME}/node_exporter >>${NODE_EXPORTER_HOME}/node.log 2>&1 &
+}
+
+function prepare_packages() {
+  if [[ -f ${HOME_DIR}/.prepared_packages ]]; then
+    logging warn "Packages already prepared, skip prepare ..."
+    return
+  fi
+
+  prepare_jdk
+  init_jdk
+
+  # add extra monitor service
+  prepare_node_exporter
+  init_node_exporter
+  # start node_exporter quickly
+  start_node_exporter
+
+  prepare_hadoop
+  init_hadoop
+  prepare_hive
+  init_hive
+
+  prepare_spark
+  init_spark
+
+  touch ${HOME_DIR}/.prepared_packages
+  logging info "All need packages are ready ..."
+}
+
+function start_services_on_master() {
+  start_spark_master
+}
+
+function main() {
+  prepare_packages
+  start_services_on_master
+}
+
+main
diff --git a/backup/scripts/prepare-ec2-env-for-spark-slave.sh b/backup/scripts/prepare-ec2-env-for-spark-slave.sh
new file mode 100644
index 0000000..7d871a5
--- /dev/null
+++ b/backup/scripts/prepare-ec2-env-for-spark-slave.sh
@@ -0,0 +1,477 @@
+#!/bin/bash
+
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# Note: this script is for AWS ec2 instances
+set -e
+
+# ============= Utils function ============
+function info() {
+  # shellcheck disable=SC2145
+  echo -e "\033[32m$@\033[0m"
+}
+
+function warn() {
+  # shellcheck disable=SC2145
+  echo -e "\033[33m$@\033[0m"
+}
+
+function error() {
+  # shellcheck disable=SC2145
+  echo -e "\033[31m$@\033[0m"
+}
+
+function logging() {
+  case $1 in
+  "info")
+    shift
+    # shellcheck disable=SC2068
+    info $@
+    ;;
+  "warn")
+    shift
+    # shellcheck disable=SC2068
+    warn $@
+    ;;
+  "error")
+    shift
+    # shellcheck disable=SC2068
+    error $@
+    ;;
+  *)
+    # shellcheck disable=SC2068
+    echo -e $@
+    ;;
+  esac
+}
+
+set +e
+
+function help() {
+  logging warn "Invalid input."
+  logging warn "Usage: ${BASH_SOURCE[0]}
+                    --bucket-url /path/to/bucket
+                    --master-host host_ip
+                    --worker-number 1 (or 2 or 3 ....)
+                    --region region-for-s3
+                    --waiting-time time-for-start-services
+                    --mode cluster-mode-is-product-or-test
+                    --local-soft whether-to-use-local-cache+soft-affinity"
+  exit 0
+}
+
+if [[ $# -ne 14 ]]; then
+  help
+fi
+
+while [[ $# != 0 ]]; do
+  if [[ $1 == "--bucket-url" ]]; then
+    BUCKET_SUFFIX=$2
+  elif [[ $1 == "--master-host" ]]; then
+    MASTER_HOST=$2
+  elif [[ $1 == "--worker-number" ]]; then
+    WORKER=$2
+  elif [[ $1 == "--region" ]]; then
+      CURRENT_REGION=$2
+  elif [[ $1 == "--waiting-time" ]]; then
+      WAITING_TIME=$2
+  elif [[ $1 == "--mode" ]]; then
+      WORKER_MODE=$2
+  elif [[ $1 == "--local-soft" ]]; then
+    LOCAL_CACHE_SOFT_AFFINITY=$2
+  else
+    help
+  fi
+  shift
+  shift
+done
+
+# =============== Env Parameters =================
+# Prepare Steps
+## Parameter
+### Parameters for Spark
+#### ${SPARK_VERSION:0:1} get 2 from 2.4.7
+HADOOP_VERSION=3.2.0
+SPARK_VERSION=3.1.1
+KYLIN_VERSION=4.0.0
+
+### Parameter for JDK 1.8
+JDK_PACKAGE=jdk-8u301-linux-x64.tar.gz
+JDK_DECOMPRESS_NAME=jdk1.8.0_301
+
+LOCAL_CACHE_DIR=/home/ec2-user/ssd
+
+### File name
+if [[ $LOCAL_CACHE_SOFT_AFFINITY == "false" ]]; then
+  KYLIN_PACKAGE=apache-kylin-${KYLIN_VERSION}-bin-spark${SPARK_VERSION:0:1}.tar.gz
+  DECOMPRESSED_KYLIN_PACKAGE=apache-kylin-${KYLIN_VERSION}-bin-spark${SPARK_VERSION:0:1}
+else
+  KYLIN_PACKAGE=apache-kylin-${KYLIN_VERSION}-bin-spark${SPARK_VERSION:0:1}-soft.tar.gz
+  DECOMPRESSED_KYLIN_PACKAGE=apache-kylin-${KYLIN_VERSION}-bin-spark${SPARK_VERSION:0:1}-soft
+
+  # Prepared the local cache dir for local cache + soft affinity
+
+  if [[ ! -d ${LOCAL_CACHE_DIR} ]]; then
+      sudo mkdir -p ${LOCAL_CACHE_DIR}
+      sudo chmod -R 777 ${LOCAL_CACHE_DIR}
+  fi
+
+  if [[ ! -d ${LOCAL_CACHE_DIR}/alluxio-cache-driver ]]; then
+      sudo mkdir -p ${LOCAL_CACHE_DIR}/alluxio-cache-driver
+      sudo chmod -R 777 ${LOCAL_CACHE_DIR}/alluxio-cache-driver
+  fi
+fi
+
+SPARK_PACKAGE=spark-${SPARK_VERSION}-bin-hadoop${HADOOP_VERSION:0:3}.tgz
+HADOOP_PACKAGE=hadoop-${HADOOP_VERSION}.tar.gz
+NODE_EXPORTER_PACKAGE=node_exporter-1.3.1.linux-amd64.tar.gz
+
+HOME_DIR=/home/ec2-user
+
+function init_env() {
+    HADOOP_DIR=${HOME_DIR}/hadoop
+    if [[ ! -d $HADOOP_DIR ]]; then
+      mkdir ${HADOOP_DIR}
+    fi
+
+    JAVA_HOME=/usr/local/java
+    JRE_HOME=${JAVA_HOME}/jre
+    KYLIN_HOME=${HOME_DIR}/${DECOMPRESSED_KYLIN_PACKAGE}
+    SPARK_HOME=${HADOOP_DIR}/spark
+    OUT_LOG=${HOME_DIR}/shell.stdout
+    HADOOP_HOME=${HADOOP_DIR}/hadoop-${HADOOP_VERSION}
+
+    # extra prometheus env
+    NODE_EXPORTER_HOME=/home/ec2-user/node_exporter
+
+    cat <<EOF >>~/.bash_profile
+## Set env variables
+export JAVA_HOME=${JAVA_HOME}
+export JRE_HOME=${JRE_HOME}
+export CLASSPATH=.:${JAVA_HOME}/lib:${JRE_HOME}/lib
+#### hadoop env
+export HADOOP_HOME=${HADOOP_HOME}
+
+### prometheus related env
+export NODE_EXPORTER_HOME=${NODE_EXPORTER_HOME}
+
+export PATH=${HADOOP_HOME}/bin:${JAVA_HOME}/bin:$PATH
+
+### other
+export HOME_DIR=${HOME_DIR}
+export KYLIN_HOME=${KYLIN_HOME}
+export SPARK_HOME=${SPARK_HOME}
+export OUT_LOG=$OUT_LOG
+EOF
+}
+
+if [[ ! -f ~/.inited_env ]]; then
+    logging info "Env variables not init, init it first ..."
+    init_env
+    touch ~/.inited_env
+else
+    logging warn "Env variables already inited, source it ..."
+fi
+
+source ~/.bash_profile
+exec 2>>${OUT_LOG}
+set -o pipefail
+# ================ Main Functions ======================
+
+PATH_TO_BUCKET=s3:/${BUCKET_SUFFIX}
+
+# Main Functions and Steps
+## prepare jdk env
+function prepare_jdk() {
+  logging info "Preparing Jdk ..."
+  if [[ -f ${HOME_DIR}/.prepared_jdk ]]; then
+    logging warn "Jdk already prepared, enjoy it."
+    return
+  fi
+
+  # copy jdk from s3 bucket to ec2 instances, so user need to upload jdk package first
+  logging info "Scp jdk package from s3: aws s3 cp ${PATH_TO_BUCKET}/tar/${JDK_PACKAGE} ${HOME_DIR} ..."
+  aws s3 cp ${PATH_TO_BUCKET}/tar/${JDK_PACKAGE} ${HOME_DIR} --region ${CURRENT_REGION}
+  # unzip jdk: tar -C /extract/to/path -xzvf /path/to/archive.tar.gz
+  tar -zxf ${JDK_PACKAGE}
+  sudo mv ${JDK_DECOMPRESS_NAME} ${JAVA_HOME}
+  if [[ $? -ne 0 ]]; then
+      logging error "Java package was not installed well, pleas check ..."
+      exit 0
+  fi
+  logging info "Jdk inited ..."
+  touch ${HOME_DIR}/.prepared_jdk
+  logging info "Jdk is ready ..."
+}
+
+function init_jdk() {
+    if [[ -f ${HOME_DIR}/.inited_jdk ]]; then
+        logging warn "Jdk already inited, skip init ..."
+        return
+    fi
+    # this function is remove the unsupport tls rules in java which version of 1.8.291 and above
+    ## backup
+    cp -f ${JAVA_HOME}/jre/lib/security/java.security ${JAVA_HOME}/jre/lib/security/java.security.bak
+
+    ## modify the java.security file
+    sed -e "s/\ TLSv1,\ TLSv1.1,//g" -i ${JAVA_HOME}/jre/lib/security/java.security
+    logging info "Jdk inited ..."
+    touch ${HOME_DIR}/.inited_jdk
+}
+
+function prepare_hadoop() {
+  if [[ -f ${HOME_DIR}/.prepared_hadoop ]]; then
+    logging warn "Hadoop already prepared, skip init ..."
+    return
+  fi
+
+  if [[ -f ${HOME_DIR}/${HADOOP_PACKAGE} ]]; then
+      logging warn "Hadoop package ${HADOOP_PACKAGE} already downloaded, skip download ..."
+  else
+      logging info "Downloading Hadoop package ${HADOOP_PACKAGE} ..."
+      aws s3 cp ${PATH_TO_BUCKET}/tar/${HADOOP_PACKAGE} ${HOME_DIR} --region ${CURRENT_REGION}
+#      # wget cost lot time
+#      wget https://archive.apache.org/dist/hadoop/common/hadoop-${HADOOP_VERSION}/${HADOOP_PACKAGE}
+  fi
+
+  if [[ -d ${HOME_DIR}/hadoop-${HADOOP_VERSION} ]]; then
+      logging warn "Hadoop Package decompressed, skip decompress ..."
+  else
+      logging info "Decompress Hadoop package ..."
+      tar -zxf ${HADOOP_PACKAGE}
+  fi
+
+  logging info "Moving hadoop package to ${HADOOP_HOME} ..."
+  mv ${HOME_DIR}/hadoop-${HADOOP_VERSION} ${HADOOP_HOME}
+
+  logging info "Hadoop prepared ..."
+  touch ${HOME_DIR}/.prepared_hadoop
+}
+
+function prepare_spark() {
+  if [[ -f ${HOME_DIR}/.prepared_spark ]]; then
+    logging warn "Spark already prepared, enjoy it."
+    return
+  fi
+
+  logging info "Downloading Spark-${SPARK_VERSION} ..."
+  ## download spark
+  if [[ -f ${HOME_DIR}/${SPARK_PACKAGE} ]]; then
+      logging warn "${SPARK_PACKAGE} already download, skip download it."
+  else
+      logging warn "Downloading ${SPARK_PACKAGE} ..."
+      aws s3 cp ${PATH_TO_BUCKET}/tar/${SPARK_PACKAGE} ${HOME_DIR} --region ${CURRENT_REGION}
+#      # wget cost lot time
+#      wget http://archive.apache.org/dist/spark/spark-${SPARK_VERSION}/${SPARK_PACKAGE}
+  fi
+
+  if [[ -d  ${HOME_DIR}/${SPARK_PACKAGE%*.*} ]]; then
+      logging warn "Spark package already decompressed, skip decompress ..."
+  else
+      logging info "Decompressing ${SPARK_PACKAGE} ..."
+      ### unzip spark tar file
+      tar -zxf ${SPARK_PACKAGE}
+  fi
+
+  if [[ -d ${SPARK_HOME} ]]; then
+    logging warn "${SPARK_HOME} already exists, skip moving ..."
+  else
+    logging info "Moving ${SPARK_PACKAGE%*.*} to ${SPARK_HOME}"
+    mv ${SPARK_PACKAGE%*.*} ${SPARK_HOME}
+  fi
+
+  logging info "Prepare Spark-${SPARK_VERSION} success."
+  touch ${HOME_DIR}/.prepared_spark
+}
+
+function init_spark() {
+  if [[ -f ${HOME_DIR}/.inited_spark ]]; then
+    logging warn "Spark already inited, enjoy it."
+    return
+  fi
+
+  # copy needed jars
+  if [[ ! -f $SPARK_HOME/jars/hadoop-aws-${HADOOP_VERSION}.jar ]]; then
+    cp $HADOOP_HOME/share/hadoop/tools/lib/hadoop-aws-${HADOOP_VERSION}.jar $SPARK_HOME/jars/
+    if [[ $? -ne 0 ]]; then
+        logging error "Copy $HADOOP_HOME/share/hadoop/tools/lib/hadoop-aws-${HADOOP_VERSION}.jar $SPARK_HOME/jars/ failed ..."
+        exit 0
+    fi
+  fi
+
+  if [[ ! -f $SPARK_HOME/jars/aws-java-sdk-bundle-1.11.375.jar ]]; then
+    cp $HADOOP_HOME/share/hadoop/tools/lib/aws-java-sdk-bundle-1.11.375.jar $SPARK_HOME/jars/
+    if [[ $? -ne 0 ]]; then
+        logging error "Copy $HADOOP_HOME/share/hadoop/tools/lib/aws-java-sdk-bundle-1.11.375.jar $SPARK_HOME/jars/ failed ..."
+        exit 0
+    fi
+  fi
+
+  #Support local cache + soft affinity
+  if [[ $LOCAL_CACHE_SOFT_AFFINITY == "true" ]]; then
+    if [[ ! -f $SPARK_HOME/jars/kylin-soft-affinity-cache-4.0.0-SNAPSHOT.jar ]]; then
+      logging info "Downloading kylin-soft-affinity-cache-4.0.0-SNAPSHOT.jar to $SPARK_HOME/jars/ ..."
+      aws s3 cp ${PATH_TO_BUCKET}/jars/kylin-soft-affinity-cache-4.0.0-SNAPSHOT.jar $SPARK_HOME/jars/ --region ${CURRENT_REGION}
+    fi
+
+    if [[ ! -f $SPARK_HOME/jars/alluxio-2.6.1-client.jar ]]; then
+      logging info "Downloading alluxio-2.6.1-client.jar to $SPARK_HOME/jars/ ..."
+      aws s3 cp ${PATH_TO_BUCKET}/jars/alluxio-2.6.1-client.jar $SPARK_HOME/jars/ --region ${CURRENT_REGION}
+    fi
+  fi
+
+  # Support prometheus metrics
+  cat <<EOF > ${SPARK_HOME}/conf/metrics.properties
+*.sink.prometheusServlet.class=org.apache.spark.metrics.sink.PrometheusServlet
+*.sink.prometheusServlet.path=/metrics/prometheus
+master.sink.prometheusServlet.path=/metrics/master/prometheus
+applications.sink.prometheusServlet.path=/metrics/applications/prometheus
+EOF
+
+  logging info "Spark inited ..."
+  touch ${HOME_DIR}/.inited_spark
+  logging info "Spark is ready ..."
+}
+
+function start_spark_worker() {
+  # TODO: fix hard code for waiting time
+  sleep ${WAITING_TIME}
+  if [[ $WORKER_MODE == 'product' ]]; then
+    # product:
+    # # ec2 instance type is m5.4xlarge which has 16 cores! Set 15 to Spark master.
+    # # Also set 60 GB memory for cluster
+    $SPARK_HOME/sbin/start-worker.sh ${MASTER_HOST}:7077 -c 15 -m 63G
+  else
+    # test: ec2 instance type is m5.2xlarget which has 8cores!
+    $SPARK_HOME/sbin/start-worker.sh ${MASTER_HOST}:7077
+  fi
+  if [[ $? -ne 0 ]]; then
+      logging error "spark start worker ${WORKER} failed, please check ..."
+      exit 0
+  fi
+  logging info "Start Spark worker ${WORKER} successfully ..."
+}
+
+function prepare_kylin() {
+  logging info "Preparing kylin ..."
+
+  if [[ -f ${HOME_DIR}/.prepared_kylin ]]; then
+      logging warn "Kylin already prepared ..."
+      return
+  fi
+
+  if [[ -f ${HOME_DIR}/${KYLIN_PACKAGE} ]]; then
+      logging warn "Kylin package already downloaded, skip download it ..."
+  else
+      logging info "Kylin-${KYLIN_VERSION} downloading ..."
+      aws s3 cp ${PATH_TO_BUCKET}/tar/${KYLIN_PACKAGE} ${HOME_DIR} --region ${CURRENT_REGION}
+#      # wget cost lot time
+#      wget https://archive.apache.org/dist/kylin/apache-kylin-${KYLIN_VERSION}/${KYLIN_PACKAGE}
+  fi
+
+  if [[ -d ${HOME_DIR}/${DECOMPRESSED_KYLIN_PACKAGE} ]]; then
+      logging warn "Kylin package already decompress, skip decompress ..."
+  else
+      logging warn "Kylin package decompressing ..."
+      ### unzip kylin tar file
+      tar -zxf ${KYLIN_PACKAGE}
+  fi
+
+  logging info "Kylin inited ..."
+  touch ${HOME_DIR}/.prepared_kylin
+  logging info "Kylin prepared ..."
+}
+
+
+function prepare_node_exporter() {
+  logging info "Preparing node_exporter ..."
+  if [[ -f ${HOME_DIR}/.prepared_node_exporter ]]; then
+      logging warn "NODE_EXPORTER already prepared, skip prepare ... "
+      return
+  fi
+
+  if [[ ! -f ${HOME_DIR}/${NODE_EXPORTER_PACKAGE} ]]; then
+      logging info "NODE_EXPORTER package ${NODE_EXPORTER_PACKAGE} not downloaded, downloading it ..."
+      aws s3 cp ${PATH_TO_BUCKET}/tar/${NODE_EXPORTER_PACKAGE} ${HOME_DIR} --region ${CURRENT_REGION}
+  else
+      logging warn "NODE_EXPORTER package ${NODE_EXPORTER_PACKAGE} already download, skip download it."
+  fi
+  touch ${HOME_DIR}/.prepared_node_exporter
+  logging info "NODE_EXPORTER prepared ..."
+}
+
+function init_node_exporter() {
+  logging info "Initializing node_exporter ..."
+  if [[ -f ${HOME_DIR}/.inited_node_exporter ]]; then
+      logging warn "NODE_EXPORTER already inited, skip init ... "
+      return
+  fi
+
+  if [[ ! -f ${NODE_EXPORTER_HOME} ]]; then
+      logging info "NODE_EXPORTER home ${NODE_EXPORTER_HOME} not ready, decompressing ${NODE_EXPORTER_PACKAGE} ..."
+      tar -zxf ${HOME_DIR}/${NODE_EXPORTER_PACKAGE}
+      mv ${HOME_DIR}/${NODE_EXPORTER_PACKAGE%.tar.gz} ${NODE_EXPORTER_HOME}
+  else
+      logging warn "NODE_EXPORTER home ${PROMETHEUS_PACKAGE} already ready."
+  fi
+  touch ${HOME_DIR}/.inited_node_exporter
+  logging info "NODE_EXPORTER inited ..."
+}
+
+function start_node_exporter() {
+    # NOTE: default node_exporter port 9100
+    logging info "Start node_exporter ..."
+    nohup ${NODE_EXPORTER_HOME}/node_exporter >> ${NODE_EXPORTER_HOME}/node.log 2>&1 &
+}
+
+function prepare_packages() {
+  if [[ -f ${HOME_DIR}/.prepared_packages ]]; then
+      logging warn "Packages already prepared, skip prepare ..."
+      return
+  fi
+
+  prepare_jdk
+  init_jdk
+
+  # add extra monitor service
+  prepare_node_exporter
+  init_node_exporter
+  # start node_exporter quickly
+  start_node_exporter
+
+  prepare_hadoop
+
+  prepare_spark
+  init_spark
+
+  prepare_kylin
+
+  touch ${HOME_DIR}/.prepared_packages
+  logging info "All need packages are ready ..."
+}
+
+function start_services_on_slave() {
+  start_spark_worker
+}
+
+function main() {
+  prepare_packages
+  start_services_on_slave
+}
+
+main
diff --git a/backup/scripts/prepare-ec2-env-for-static-services.sh b/backup/scripts/prepare-ec2-env-for-static-services.sh
new file mode 100644
index 0000000..02e02dc
--- /dev/null
+++ b/backup/scripts/prepare-ec2-env-for-static-services.sh
@@ -0,0 +1,622 @@
+#!/bin/bash
+
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# Note: this script is for Creating Zookeeper and DB to hold cluster
+# This Script contains services which are Zookeeper, JDK and Mysql DB
+set -e
+
+# ============= Utils function ============
+function info() {
+  # shellcheck disable=SC2145
+  echo -e "\033[32m$@\033[0m"
+}
+
+function warn() {
+  # shellcheck disable=SC2145
+  echo -e "\033[33m$@\033[0m"
+}
+
+function error() {
+  # shellcheck disable=SC2145
+  echo -e "\033[31m$@\033[0m"
+}
+
+function logging() {
+  case $1 in
+  "info")
+    shift
+    # shellcheck disable=SC2068
+    info $@
+    ;;
+  "warn")
+    shift
+    # shellcheck disable=SC2068
+    warn $@
+    ;;
+  "error")
+    shift
+    # shellcheck disable=SC2068
+    error $@
+    ;;
+  *)
+    # shellcheck disable=SC2068
+    echo -e $@
+    ;;
+  esac
+}
+
+set +e
+
+# =============== Env Parameters =================
+# Prepare Steps
+## Parameter
+EC2_DEFAULT_USER=ec2-user
+
+### Parameters for Spark and Kylin
+#### ${SPARK_VERSION:0:1} get 2 from 2.4.7
+GRAFANA_VERSION=8.2.6
+HIVE_VERSION=2.3.9
+HADOOP_VERSION=3.2.0
+
+### File name
+PROMETHEUS_PACKAGE=prometheus-2.31.1.linux-amd64.tar.gz
+NODE_EXPORTER_PACKAGE=node_exporter-1.3.1.linux-amd64.tar.gz
+HIVE_PACKAGE=apache-hive-${HIVE_VERSION}-bin.tar.gz
+HADOOP_PACKAGE=hadoop-${HADOOP_VERSION}.tar.gz
+
+### Parameter for JDK 1.8
+JDK_PACKAGE=jdk-8u301-linux-x64.tar.gz
+JDK_DECOMPRESS_NAME=jdk1.8.0_301
+
+HOME_DIR=/home/ec2-user
+
+function init_env() {
+  HADOOP_DIR=${HOME_DIR}/hadoop
+  if [[ ! -d $HADOOP_DIR ]]; then
+    mkdir ${HADOOP_DIR}
+  fi
+
+  JAVA_HOME=/usr/local/java
+  JRE_HOME=${JAVA_HOME}/jre
+  OUT_LOG=${HOME_DIR}/shell.stdout
+  HADOOP_HOME=${HADOOP_DIR}/hadoop-${HADOOP_VERSION}
+  HIVE_HOME=${HADOOP_DIR}/hive
+
+  # extra prometheus env
+  PROMETHEUS_HOME=/home/ec2-user/prometheus
+  NODE_EXPORTER_HOME=/home/ec2-user/node_exporter
+
+  cat <<EOF >>~/.bash_profile
+## Set env variables
+### jdk env
+export JAVA_HOME=${JAVA_HOME}
+export JRE_HOME=${JRE_HOME}
+export CLASSPATH=.:${JAVA_HOME}/lib:${JRE_HOME}/lib
+
+### hadoop env
+export HADOOP_HOME=${HADOOP_HOME}
+
+### hive env
+export HIVE_HOME=${HIVE_HOME}
+
+### prometheus related env
+export PROMETHEUS_HOME=${PROMETHEUS_HOME}
+export NODE_EXPORTER_HOME=${NODE_EXPORTER_HOME}
+
+### export all path
+export PATH=$HIVE_HOME/bin:$HIVE_HOME/conf:${HADOOP_HOME}/bin:${JAVA_HOME}/bin:$PATH
+
+### other
+export HOME_DIR=${HOME_DIR}
+export OUT_LOG=$OUT_LOG
+EOF
+}
+
+if [[ ! -f ~/.inited_env ]]; then
+  logging info "Env variables not init, init it first ..."
+  init_env
+  touch ~/.inited_env
+else
+  logging warn "Env variables already inited, source it ..."
+fi
+source ~/.bash_profile
+exec 2>>${OUT_LOG}
+set -o pipefail
+# ================ Main Functions ======================
+function help() {
+  logging warn "Invalid input."
+  logging warn "Usage: ${BASH_SOURCE[0]}
+                       --bucket-url /path/to/bucket/without/prefix
+                       --region region-for-current-instance
+                       --db-host host-for-hive-to-access-rds
+                       --db-user user-for-hive-to-access-rds
+                       --db-password password-for-hive-to-access-rds
+                       --db-port port-for-hive-to-access-rds"
+  exit 0
+}
+
+if [[ $# -ne 12 ]]; then
+  help
+fi
+
+while [[ $# != 0 ]]; do
+  if [[ $1 == "--bucket-url" ]]; then
+    # url same as: /xxx/kylin
+    BUCKET_SUFFIX=$2
+  elif [[ $1 == "--region" ]]; then
+    CURRENT_REGION=$2
+  elif [[ $1 == "--db-host" ]]; then
+    DATABASE_HOST=$2
+  elif [[ $1 == "--db-password" ]]; then
+    DATABASE_PASSWORD=$2
+  elif [[ $1 == "--db-user" ]]; then
+    DATABASE_USER=$2
+  elif [[ $1 == "--db-port" ]]; then
+    DATABASE_PORT=$2
+  else
+    help
+  fi
+  shift
+  shift
+done
+
+PATH_TO_BUCKET=s3:/${BUCKET_SUFFIX}
+CONFIG_PATH_TO_BUCKET=s3a:/${BUCKET_SUFFIX}
+
+# Main Functions and Steps
+## prepare jdk env
+function prepare_jdk() {
+  logging info "Preparing Jdk ..."
+  if [[ -f ${HOME_DIR}/.prepared_jdk ]]; then
+    logging warn "Jdk already prepared, enjoy it."
+    return
+  fi
+
+  # copy jdk from s3 bucket to ec2 instances, so user need to upload jdk package first
+  aws s3 cp ${PATH_TO_BUCKET}/tar/${JDK_PACKAGE} ${HOME_DIR} --region ${CURRENT_REGION}
+  # unzip jdk: tar -C /extract/to/path -xzvf /path/to/archive.tar.gz
+  tar -zxf ${JDK_PACKAGE}
+  sudo mv ${JDK_DECOMPRESS_NAME} ${JAVA_HOME}
+  if [[ $? -ne 0 ]]; then
+    logging error "Java package was not installed well, pleas check ..."
+    exit 0
+  fi
+  logging info "Jdk inited ..."
+  touch ${HOME_DIR}/.prepared_jdk
+  logging info "Jdk is ready ..."
+}
+
+function init_jdk() {
+  if [[ -f ${HOME_DIR}/.inited_jdk ]]; then
+    logging warn "Jdk already inited, skip init ..."
+    return
+  fi
+  # this function is remove the unsupport tls rules in java which version of 1.8.291 and above
+  ## backup
+  cp -f $JAVA_HOME/jre/lib/security/java.security $JAVA_HOME/jre/lib/security/java.security.bak
+
+  ## modify the java.security file
+  sed -e "s/\ TLSv1,\ TLSv1.1,//g" -i $JAVA_HOME/jre/lib/security/java.security
+  logging info "Jdk inited ..."
+  touch ${HOME_DIR}/.inited_jdk
+}
+
+function prepare_hadoop() {
+  if [[ -f ${HOME_DIR}/.prepared_hadoop ]]; then
+    logging warn "Hadoop already prepared, skip init ..."
+    return
+  fi
+
+  if [[ -f ${HOME_DIR}/${HADOOP_PACKAGE} ]]; then
+    logging warn "Hadoop package ${HADOOP_PACKAGE} already downloaded, skip download ..."
+  else
+    logging info "Downloading Hadoop package ${HADOOP_PACKAGE} ..."
+    aws s3 cp ${PATH_TO_BUCKET}/tar/${HADOOP_PACKAGE} ${HOME_DIR} --region ${CURRENT_REGION}
+    #      # wget cost lot time
+    #      wget https://archive.apache.org/dist/hadoop/common/hadoop-${HADOOP_VERSION}/${HADOOP_PACKAGE}
+  fi
+
+  if [[ -d ${HOME_DIR}/hadoop-${HADOOP_VERSION} ]]; then
+    logging warn "Hadoop Package decompressed, skip decompress ..."
+  else
+    logging info "Decompress Hadoop package ..."
+    tar -zxf ${HADOOP_PACKAGE}
+  fi
+
+  logging info "Hadoop prepared ..."
+  touch ${HOME_DIR}/.prepared_hadoop
+}
+
+function init_hadoop() {
+  # match correct region endpoints for hadoop's core-site.xml
+  if [[ ${CURRENT_REGION} == "cn-northwest-1" ]]; then
+    S3_ENDPOINT=s3.cn-northwest-1.amazonaws.com.cn
+  elif [[ ${CURRENT_REGION} == "cn-north-1" ]]; then
+    S3_ENDPOINT=s3.cn-north-1.amazonaws.com.cn
+  else
+    S3_ENDPOINT=s3.${CURRENT_REGION}.amazonaws.com
+  fi
+
+  if [[ -f ${HOME_DIR}/.inited_hadoop ]]; then
+    logging warn "Hadoop already inited, skip init ..."
+  else
+    logging info "Init hadoop config ..."
+    # replace jars for hadoop, although it don't need to start
+    cp hadoop-${HADOOP_VERSION}/share/hadoop/tools/lib/aws-java-sdk-bundle-1.11.375.jar hadoop-${HADOOP_VERSION}/share/hadoop/common/lib/
+    cp hadoop-${HADOOP_VERSION}/share/hadoop/tools/lib/hadoop-aws-${HADOOP_VERSION}.jar hadoop-${HADOOP_VERSION}/share/hadoop/common/lib/
+
+    cat <<EOF >${HOME_DIR}/hadoop-${HADOOP_VERSION}/etc/hadoop/core-site.xml
+<?xml version="1.0" encoding="UTF-8"?>
+<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
+<configuration>
+	<property>
+	    <name>fs.default.name</name>
+	    <value>${CONFIG_PATH_TO_BUCKET}/working_dir</value>
+	</property>
+  <property>
+    <name>fs.s3a.endpoint</name>
+    <value>${S3_ENDPOINT}</value>
+  </property>
+</configuration>
+EOF
+
+    logging info "Moving hadoop package to ${HADOOP_HOME} ..."
+    mv ${HOME_DIR}/hadoop-${HADOOP_VERSION} ${HADOOP_HOME}
+
+    logging warn "touch ${HOME_DIR}/.inited_hadoop ... "
+    touch ${HOME_DIR}/.inited_hadoop
+  fi
+
+  logging info "Hadoop is ready ..."
+}
+
+function prepare_hive() {
+  logging info "Preparing hive ..."
+  if [[ -f ${HOME_DIR}/.prepared_hive ]]; then
+    logging warn "Hive already prepared, enjoy it."
+    return
+  fi
+
+  if [[ -f ${HOME_DIR}/${HIVE_PACKAGE} ]]; then
+    logging warn "${HIVE_PACKAGE} already downloaded, skip download it ..."
+  else
+    logging info "Downloading ${HIVE_PACKAGE} ..."
+    aws s3 cp ${PATH_TO_BUCKET}/tar/${HIVE_PACKAGE} ${HOME_DIR} --region ${CURRENT_REGION}
+    #      # wget cost lot time
+    #      wget https://downloads.apache.org/hive/hive-${HIVE_VERSION}/${HIVE_PACKAGE}
+  fi
+
+  if [[ -d ${HOME_DIR}/apache-hive-${HIVE_VERSION}-bin ]]; then
+    logging warn "Hive Package already decompressed, skip decompress ..."
+  else
+    logging info "Decompress Hive package ..."
+    tar -zxf ${HIVE_PACKAGE}
+  fi
+
+  if [[ -d ${HIVE_HOME} ]]; then
+    logging warn "Hive package already exists, skip moving ..."
+  else
+    logging info "Moving hive package ${HIVE_PACKAGE%*.*.*} to ${HIVE_HOME} ..."
+    mv ${HIVE_PACKAGE%*.*.*} ${HIVE_HOME}
+    if [[ $? -ne 0 ]]; then
+      logging error " Moving hive package failed, please check ..."
+      exit 0
+    fi
+  fi
+
+  # execute command
+  hive --version
+  if [[ $? -eq 0 ]]; then
+    logging info "Hive ${HIVE_VERSION} is ready ..."
+  else
+    logging error "Hive not prepared well, please check ..."
+    exit 0
+  fi
+
+  logging info "Hive prepared successfully ..."
+  touch ${HOME_DIR}/.prepared_hive
+}
+
+function init_hive() {
+  if [[ -f ${HOME_DIR}/.inited_hive ]]; then
+    logging warn "Hive already init, skip init ..."
+    return
+  fi
+
+  cat <<EOF >${HIVE_HOME}/conf/hive-site.xml
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
+<configuration>
+  <property>
+    <name>javax.jdo.option.ConnectionPassword</name>
+    <value>${DATABASE_PASSWORD}</value>
+    <description>password to use against metastore database</description>
+  </property>
+  <property>
+    <name>javax.jdo.option.ConnectionURL</name>
+    <value>jdbc:mysql://${DATABASE_HOST}:${DATABASE_PORT}/hive?createDatabaseIfNotExist=true&amp;useSSL=false</value>
+    <description>JDBC connect string for a JDBC metastore</description>
+  </property>
+  <property>
+    <name>javax.jdo.option.ConnectionDriverName</name>
+    <value>com.mysql.jdbc.Driver</value>
+    <description>Driver class name for a JDBC metastore</description>
+  </property>
+  <property>
+    <name>javax.jdo.option.ConnectionUserName</name>
+    <value>${DATABASE_USER}</value>
+    <description>Username to use against metastore database;default is root</description>
+  </property>
+  <property>
+    <name>hive.metastore.schema.verification</name>
+    <value>false</value>
+    <description>
+      Enforce metastore schema version consistency.
+      True: Verify that version information stored in metastore matches with one from Hive jars.  Also disable automatic
+            schema migration attempt. Users are required to manually migrate schema after Hive upgrade which ensures
+            proper metastore schema migration. (Default)
+      False: Warn if the version information stored in metastore doesn't match with one from in Hive jars.
+    </description>
+  </property>
+</configuration>
+EOF
+
+  # resolve jars conflict
+  if [[ ! -d $HIVE_HOME/spark_jar ]]; then
+    mkdir -p $HIVE_HOME/spark_jar
+    mv $HIVE_HOME/lib/spark-* $HIVE_HOME/spark_jar/
+    mv $HIVE_HOME/lib/jackson-module-scala_2.11-2.6.5.jar $HIVE_HOME/spark_jar/
+  fi
+
+  if [[ ! -f ${HIVE_HOME}/lib/mysql-connector-java-5.1.40.jar ]]; then
+    logging warn "${HIVE_HOME}/lib/mysql-connector-java-5.1.40.jar not exist, download it ..."
+    aws s3 cp ${PATH_TO_BUCKET}/jars/mysql-connector-java-5.1.40.jar ${HIVE_HOME}/lib/ --region ${CURRENT_REGION}
+  fi
+
+  if [[ ! -f ${HOME_DIR}/.inited_hive_metadata ]]; then
+    bash -vx $HIVE_HOME/bin/schematool -dbType mysql -initSchema
+    if [[ $? -ne 0 ]]; then
+      logging error "Init hive metadata failed, Maybe it was already initialized, please check ..."
+    else
+      logging info "Hive metadata inited successfully ..."
+    fi
+    touch ${HOME_DIR}/.inited_hive_metadata
+  fi
+
+  if [[ ! -d ${HIVE_HOME}/logs ]]; then
+    logging info "Making dir ${HIVE_HOME}/logs"
+    mkdir -p $HIVE_HOME/logs
+  fi
+
+  logging warn "touch ${HOME_DIR}/.inited_hive ..."
+  # make a tag for hive inited
+  touch ${HOME_DIR}/.inited_hive
+  logging info "Hive is ready ..."
+}
+
+function start_hive_metastore() {
+  nohup $HIVE_HOME/bin/hive --service metastore >> $HIVE_HOME/logs/hivemetastorelog.log 2>&1 &
+  logging info "Hive was logging in $HIVE_HOME/logs, you can check ..."
+}
+
+## install mysql db with docker
+function start_docker() {
+  # check docker whether is running
+  docker_status=$(systemctl is-active docker)
+  if [[ $docker_status == "active" ]]; then
+    logging warn "Docker service is already running, don't need to start it ..."
+  else
+    logging warn "Docker service is stopped, starting it ..."
+    sudo systemctl start docker
+    docker_status=$(systemctl is-active docker)
+    if [[ $docker_status == "inactive" ]]; then
+      logging error "Start docker failed, please check."
+      exit 0
+    fi
+    logging info "Start docker success ..."
+  fi
+}
+
+function prepare_docker() {
+  logging info "Preparing docker ..."
+
+  if [[ -f ${HOME_DIR}/.prepared_docker ]]; then
+    logging warn "Docker service already installed, restart it."
+    return
+  fi
+
+  # check docker whether is installed
+  if [[ -x "$(command -v docker)" ]]; then
+    docker_version=$(sudo docker -v)
+    logging info "Docker is already installed, version is ${docker_version}"
+  else
+    logging warn "Docker was not installed, now install docker ..."
+    sudo yum install docker -y
+  fi
+
+  if [[ $(getent group docker) ]]; then
+    logging warn "docker group exists, don't need create it."
+  else
+    logging warn "docker does not exist, create it."
+    sudo groupadd docker
+  fi
+
+  if id -Gn ${EC2_DEFAULT_USER} | grep docker; then
+    logging warn "${EC2_DEFAULT_USER} already in group of docker"
+  else
+    logging info "Group of docker add user ${EC2_DEFAULT_USER}"
+    sudo usermod -aG docker $EC2_DEFAULT_USER
+  fi
+
+  touch ${HOME_DIR}/.prepared_docker
+  logging info "docker is ready ..."
+}
+
+function start_grafana() {
+  logging info "Preparing grafana ..."
+  if [[ -f ${HOME_DIR}/.prepared_grafana ]]; then
+    logging warn "Grafana service already installed, check it."
+    return
+  fi
+
+  start_docker
+
+  if [[ $(sudo docker ps -q -f name=grafana-${GRAFANA_VERSION}) ]]; then
+    logging warn "Grafana-${GRAFANA_VERSION} already running, skip this ..."
+  else
+    # default user is root !!!
+    sudo docker run -d --name grafana-${GRAFANA_VERSION} --restart=always -p 3000:3000 grafana/grafana:${GRAFANA_VERSION}
+
+    if [[ $? -ne 0 ]]; then
+      logging error "Grafana start in docker was failed, please check ..."
+      exit 0
+    fi
+  fi
+  logging warn "touch ${HOME_DIR}/.prepared_grafana"
+  touch ${HOME_DIR}/.prepared_grafana
+  logging info "Grafana is ready ..."
+}
+
+function prepare_prometheus() {
+  logging info "Preparing prometheus ..."
+  if [[ -f ${HOME_DIR}/.prepared_prometheus ]]; then
+      logging warn "Prometheus already prepared, skip prepare ... "
+      return
+  fi
+
+  if [[ ! -f ${HOME_DIR}/${PROMETHEUS_PACKAGE} ]]; then
+      logging info "Prometheus package ${PROMETHEUS_PACKAGE} not downloaded, downloading it ..."
+      aws s3 cp ${PATH_TO_BUCKET}/tar/${PROMETHEUS_PACKAGE} ${HOME_DIR} --region ${CURRENT_REGION}
+  else
+      logging warn "Prometheus package ${PROMETHEUS_PACKAGE} already download, skip download it."
+  fi
+  touch ${HOME_DIR}/.prepared_prometheus
+  logging info "Prometheus prepared ..."
+}
+
+function init_prometheus() {
+  logging info "Initializing prometheus ..."
+  if [[ -f ${HOME_DIR}/.inited_prometheus ]]; then
+      logging warn "Prometheus already inited, skip init ... "
+      return
+  fi
+
+  if [[ ! -f ${PROMETHEUS_HOME} ]]; then
+      logging info "Prometheus home ${PROMETHEUS_HOME} not ready, decompressing ${PROMETHEUS_PACKAGE} ..."
+      tar -zxf ${HOME_DIR}/${PROMETHEUS_PACKAGE}
+      mv ${HOME_DIR}/${PROMETHEUS_PACKAGE%.tar.gz} ${PROMETHEUS_HOME}
+  else
+      logging warn "Prometheus home ${PROMETHEUS_PACKAGE} already ready."
+  fi
+
+  if [[ ! -d ${PROMETHEUS_HOME}/data ]]; then
+    logging info "Prometheus data dir not exists, creating it ..."
+    mkdir -p ${PROMETHEUS_HOME}/data
+  fi
+
+  touch ${HOME_DIR}/.inited_prometheus
+  logging info "Prometheus inited ..."
+  # NOTE: prometheus server will start after node_exporter on every node started.
+}
+
+function prepare_node_exporter() {
+  logging info "Preparing node_exporter ..."
+  if [[ -f ${HOME_DIR}/.prepared_node_exporter ]]; then
+      logging warn "NODE_EXPORTER already prepared, skip prepare ... "
+      return
+  fi
+
+  if [[ ! -f ${HOME_DIR}/${NODE_EXPORTER_PACKAGE} ]]; then
+      logging info "NODE_EXPORTER package ${NODE_EXPORTER_PACKAGE} not downloaded, downloading it ..."
+      aws s3 cp ${PATH_TO_BUCKET}/tar/${NODE_EXPORTER_PACKAGE} ${HOME_DIR} --region ${CURRENT_REGION}
+  else
+      logging warn "NODE_EXPORTER package ${NODE_EXPORTER_PACKAGE} already download, skip download it."
+  fi
+  touch ${HOME_DIR}/.prepared_node_exporter
+  logging info "NODE_EXPORTER prepared ..."
+}
+
+function init_node_exporter() {
+  logging info "Initializing node_exporter ..."
+  if [[ -f ${HOME_DIR}/.inited_node_exporter ]]; then
+      logging warn "NODE_EXPORTER already inited, skip init ... "
+      return
+  fi
+
+  if [[ ! -f ${NODE_EXPORTER_HOME} ]]; then
+      logging info "NODE_EXPORTER home ${NODE_EXPORTER_HOME} not ready, decompressing ${NODE_EXPORTER_PACKAGE} ..."
+      tar -zxf ${HOME_DIR}/${NODE_EXPORTER_PACKAGE}
+      mv ${HOME_DIR}/${NODE_EXPORTER_PACKAGE%.tar.gz} ${NODE_EXPORTER_HOME}
+  else
+      logging warn "NODE_EXPORTER home ${PROMETHEUS_PACKAGE} already ready."
+  fi
+  touch ${HOME_DIR}/.inited_node_exporter
+  logging info "NODE_EXPORTER inited ..."
+}
+
+function start_node_exporter() {
+    # NOTE: default node_exporter port 9100
+    logging info "Start node_exporter ..."
+    nohup ${NODE_EXPORTER_HOME}/node_exporter >> ${NODE_EXPORTER_HOME}/node.log 2>&1 &
+}
+
+function prepare_packages() {
+  if [[ -f ${HOME_DIR}/.prepared_packages ]]; then
+    logging warn "Packages already prepared, skip prepare ..."
+    return
+  fi
+
+  prepare_jdk
+  init_jdk
+
+  # add extra monitor service
+  prepare_prometheus
+  init_prometheus
+
+  prepare_node_exporter
+  init_node_exporter
+
+  prepare_hadoop
+  init_hadoop
+
+  prepare_hive
+  init_hive
+
+  touch ${HOME_DIR}/.prepared_packages
+  logging info "All need packages are ready ..."
+}
+
+function start_services_on_other() {
+  start_hive_metastore
+
+  # start extra monitor service
+  # NOTE: prometheus server will start after all node_exporter on every node started.
+  start_node_exporter
+
+  # grafana will start at last
+  prepare_docker
+  start_grafana
+}
+
+function main() {
+  prepare_packages
+  start_services_on_other
+}
+
+main
diff --git a/backup/scripts/prepare-ec2-env-for-zk.sh b/backup/scripts/prepare-ec2-env-for-zk.sh
new file mode 100644
index 0000000..03483d4
--- /dev/null
+++ b/backup/scripts/prepare-ec2-env-for-zk.sh
@@ -0,0 +1,336 @@
+#!/bin/bash
+
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# Note: this script is for Creating Zookeeper and DB to hold cluster
+# This Script contains services which are Zookeeper, JDK and Mysql DB
+set -e
+
+# ============= Utils function ============
+function info() {
+  # shellcheck disable=SC2145
+  echo -e "\033[32m$@\033[0m"
+}
+
+function warn() {
+  # shellcheck disable=SC2145
+  echo -e "\033[33m$@\033[0m"
+}
+
+function error() {
+  # shellcheck disable=SC2145
+  echo -e "\033[31m$@\033[0m"
+}
+
+function logging() {
+  case $1 in
+  "info")
+    shift
+    # shellcheck disable=SC2068
+    info $@
+    ;;
+  "warn")
+    shift
+    # shellcheck disable=SC2068
+    warn $@
+    ;;
+  "error")
+    shift
+    # shellcheck disable=SC2068
+    error $@
+    ;;
+  *)
+    # shellcheck disable=SC2068
+    echo -e $@
+    ;;
+  esac
+}
+
+set +e
+
+# =============== Env Parameters =================
+# Prepare Steps
+## Parameter
+### Parameters for Spark and Kylin
+#### ${SPARK_VERSION:0:1} get 2 from 2.4.7
+ZOOKEEPER_VERSION=3.4.13
+
+### File name
+ZOOKEEPER_PACKAGE=zookeeper-${ZOOKEEPER_VERSION}.tar.gz
+PROMETHEUS_PACKAGE=prometheus-2.31.1.linux-amd64.tar.gz
+NODE_EXPORTER_PACKAGE=node_exporter-1.3.1.linux-amd64.tar.gz
+
+### Parameter for JDK 1.8
+JDK_PACKAGE=jdk-8u301-linux-x64.tar.gz
+JDK_DECOMPRESS_NAME=jdk1.8.0_301
+
+HOME_DIR=/home/ec2-user
+
+CURRENT_HOST=$(hostname -i)
+
+function init_env() {
+  HADOOP_DIR=${HOME_DIR}/hadoop
+  if [[ ! -d $HADOOP_DIR ]]; then
+    mkdir ${HADOOP_DIR}
+  fi
+
+  JAVA_HOME=/usr/local/java
+  JRE_HOME=${JAVA_HOME}/jre
+  ZOOKEEPER_HOME=${HADOOP_DIR}/zookeeper
+  OUT_LOG=${HOME_DIR}/shell.stdout
+
+  # extra prometheus env
+  PROMETHEUS_HOME=/home/ec2-user/prometheus
+  NODE_EXPORTER_HOME=/home/ec2-user/node_exporter
+
+  cat <<EOF >>~/.bash_profile
+## Set env variables
+### jdk env
+export JAVA_HOME=${JAVA_HOME}
+export JRE_HOME=${JRE_HOME}
+export CLASSPATH=.:${JAVA_HOME}/lib:${JRE_HOME}/lib
+
+### zookeeper env
+export ZOOKEEPER_HOME=${ZOOKEEPER_HOME}
+
+### prometheus related env
+export PROMETHEUS_HOME=${PROMETHEUS_HOME}
+export NODE_EXPORTER_HOME=${NODE_EXPORTER_HOME}
+
+### export all path
+export PATH=${JAVA_HOME}/bin:${ZOOKEEPER_HOME}/bin:$PATH
+
+### other
+export HOME_DIR=${HOME_DIR}
+export OUT_LOG=$OUT_LOG
+EOF
+}
+
+if [[ ! -f ~/.inited_env ]]; then
+  logging info "Env variables not init, init it first ..."
+  init_env
+  touch ~/.inited_env
+else
+  logging warn "Env variables already inited, source it ..."
+fi
+source ~/.bash_profile
+exec 2>>${OUT_LOG}
+set -o pipefail
+# ================ Main Functions ======================
+function help() {
+  logging warn "Invalid input."
+  logging warn "Usage: ${BASH_SOURCE[0]}
+                       --bucket-url /path/to/bucket/without/prefix
+                       --region region-for-current-instance
+                       --zk-num current-zookeeper-number"
+  exit 0
+}
+
+if [[ $# -ne 6 ]]; then
+  help
+fi
+
+while [[ $# != 0 ]]; do
+  if [[ $1 == "--bucket-url" ]]; then
+    # url same as: /xxx/kylin
+    BUCKET_SUFFIX=$2
+  elif [[ $1 == "--region" ]]; then
+    CURRENT_REGION=$2
+  elif [[ $1 == "--zk-num" ]]; then
+    ZK_NUM=$2
+  else
+    help
+  fi
+  shift
+  shift
+done
+
+PATH_TO_BUCKET=s3:/${BUCKET_SUFFIX}
+
+# Main Functions and Steps
+## prepare jdk env
+function prepare_jdk() {
+  logging info "Preparing Jdk ..."
+  if [[ -f ${HOME_DIR}/.prepared_jdk ]]; then
+    logging warn "Jdk already prepared, enjoy it."
+    return
+  fi
+
+  # copy jdk from s3 bucket to ec2 instances, so user need to upload jdk package first
+  aws s3 cp ${PATH_TO_BUCKET}/tar/${JDK_PACKAGE} ${HOME_DIR} --region ${CURRENT_REGION}
+  # unzip jdk: tar -C /extract/to/path -xzvf /path/to/archive.tar.gz
+  tar -zxf ${JDK_PACKAGE}
+  sudo mv ${JDK_DECOMPRESS_NAME} ${JAVA_HOME}
+  if [[ $? -ne 0 ]]; then
+    logging error "Java package was not installed well, pleas check ..."
+    exit 0
+  fi
+  logging info "Jdk inited ..."
+  touch ${HOME_DIR}/.prepared_jdk
+  logging info "Jdk is ready ..."
+}
+
+function init_jdk() {
+  if [[ -f ${HOME_DIR}/.inited_jdk ]]; then
+    logging warn "Jdk already inited, skip init ..."
+    return
+  fi
+  # this function is remove the unsupport tls rules in java which version of 1.8.291 and above
+  ## backup
+  cp -f $JAVA_HOME/jre/lib/security/java.security $JAVA_HOME/jre/lib/security/java.security.bak
+
+  ## modify the java.security file
+  sed -e "s/\ TLSv1,\ TLSv1.1,//g" -i $JAVA_HOME/jre/lib/security/java.security
+  logging info "Jdk inited ..."
+  touch ${HOME_DIR}/.inited_jdk
+}
+
+function prepare_zookeeper() {
+  logging info "Preparing zookeeper ..."
+  if [[ -f ${HOME_DIR}/.prepared_zookeeper ]]; then
+    logging warn "Zookeeper service already installed, restart it."
+    return
+  fi
+
+  if [[ -f ./${ZOOKEEPER_PACKAGE} ]]; then
+    logging warn "Zookeeper package already download, skip download it"
+  else
+    logging info "Downloading Zookeeper package ${ZOOKEEPER_PACKAGE} ..."
+    aws s3 cp ${PATH_TO_BUCKET}/tar/${ZOOKEEPER_PACKAGE} ${HOME_DIR} --region ${CURRENT_REGION}
+    #      # wget cost lot time
+    #      wget http://archive.apache.org/dist/zookeeper/zookeeper-${ZOOKEEPER_VERSION}/${ZOOKEEPER_PACKAGE}
+  fi
+
+  if [[ -d ${HOME_DIR}/zookeeper-${ZOOKEEPER_VERSION} ]]; then
+    logging warn "Zookeeper Package decompressed, skip decompress ..."
+  else
+    logging info "Decompress Zookeeper package ..."
+    tar -zxf ${ZOOKEEPER_PACKAGE}
+  fi
+
+  logging info "Zookeeper prepared ..."
+  touch ${HOME_DIR}/.prepared_zookeeper
+}
+
+function init_zookeeper() {
+  if [[ -f ${HOME_DIR}/.inited_zookeeper ]]; then
+    logging warn "Zookeeper already inited ..."
+  else
+    logging info "Init Zookeeper config ..."
+    # copy cfg to set fake zk cluster
+    cp zookeeper-${ZOOKEEPER_VERSION}/conf/zoo_sample.cfg zookeeper-${ZOOKEEPER_VERSION}/conf/zoo.cfg
+
+    cat <<EOF >zookeeper-${ZOOKEEPER_VERSION}/conf/zoo.cfg
+# zoo${ZK_NUM}.cfg
+tickTime=2000
+initLimit=10
+syncLimit=5
+dataDir=/tmp/zookeeper/zk${ZK_NUM}/data
+dataLogDir=/tmp/zookeeper/zk${ZK_NUM}/log
+clientPort=2181
+# server.${ZK_NUM}=${CURRENT_HOST}:2287:3387
+EOF
+    mkdir -p /tmp/zookeeper/zk${ZK_NUM}/log
+    mkdir -p /tmp/zookeeper/zk${ZK_NUM}/data
+    echo ${ZK_NUM} >>/tmp/zookeeper/zk${ZK_NUM}/data/myid
+
+    logging info "Moving ${HOME_DIR}/zookeeper-${ZOOKEEPER_VERSION} to ${ZOOKEEPER_HOME} ..."
+    mv ${HOME_DIR}/zookeeper-${ZOOKEEPER_VERSION} ${ZOOKEEPER_HOME}
+
+    logging warn "touch ${HOME_DIR}/.inited_zookeeper ..."
+    touch ${HOME_DIR}/.inited_zookeeper
+  fi
+  logging info "Zookeeper is ready ..."
+}
+
+function prepare_node_exporter() {
+  logging info "Preparing node_exporter ..."
+  if [[ -f ${HOME_DIR}/.prepared_node_exporter ]]; then
+      logging warn "NODE_EXPORTER already prepared, skip prepare ... "
+      return
+  fi
+
+  if [[ ! -f ${HOME_DIR}/${NODE_EXPORTER_PACKAGE} ]]; then
+      logging info "NODE_EXPORTER package ${NODE_EXPORTER_PACKAGE} not downloaded, downloading it ..."
+      aws s3 cp ${PATH_TO_BUCKET}/tar/${NODE_EXPORTER_PACKAGE} ${HOME_DIR} --region ${CURRENT_REGION}
+  else
+      logging warn "NODE_EXPORTER package ${NODE_EXPORTER_PACKAGE} already download, skip download it."
+  fi
+  touch ${HOME_DIR}/.prepared_node_exporter
+  logging info "NODE_EXPORTER prepared ..."
+}
+
+function init_node_exporter() {
+  logging info "Initializing node_exporter ..."
+  if [[ -f ${HOME_DIR}/.inited_node_exporter ]]; then
+      logging warn "NODE_EXPORTER already inited, skip init ... "
+      return
+  fi
+
+  if [[ ! -f ${NODE_EXPORTER_HOME} ]]; then
+      logging info "NODE_EXPORTER home ${NODE_EXPORTER_HOME} not ready, decompressing ${NODE_EXPORTER_PACKAGE} ..."
+      tar -zxf ${HOME_DIR}/${NODE_EXPORTER_PACKAGE}
+      mv ${HOME_DIR}/${NODE_EXPORTER_PACKAGE%.tar.gz} ${NODE_EXPORTER_HOME}
+  else
+      logging warn "NODE_EXPORTER home ${PROMETHEUS_PACKAGE} already ready."
+  fi
+  touch ${HOME_DIR}/.inited_node_exporter
+  logging info "NODE_EXPORTER inited ..."
+}
+
+function start_node_exporter() {
+    # NOTE: default node_exporter port 9100
+    logging info "Start node_exporter ..."
+    nohup ${NODE_EXPORTER_HOME}/node_exporter >> ${NODE_EXPORTER_HOME}/node.log 2>&1 &
+}
+
+function prepare_packages() {
+  if [[ -f ${HOME_DIR}/.prepared_packages ]]; then
+    logging warn "Packages already prepared, skip prepare ..."
+    return
+  fi
+
+  prepare_jdk
+  init_jdk
+
+  # add extra monitor service
+  prepare_node_exporter
+  init_node_exporter
+
+  prepare_zookeeper
+  init_zookeeper
+
+  touch ${HOME_DIR}/.prepared_packages
+  logging info "All need packages are ready ..."
+}
+
+function start_services_on_other() {
+  # Note: start zookeeper after created all needed nodes, because need to update every zk cfg.
+
+  # start extra monitor service
+  # NOTE: prometheus server will start after all node_exporter on every node started.
+  start_node_exporter
+
+}
+
+function main() {
+  prepare_packages
+  start_services_on_other
+}
+
+main
diff --git a/bin/init.sh b/bin/init.sh
new file mode 100755
index 0000000..88fb704
--- /dev/null
+++ b/bin/init.sh
@@ -0,0 +1,81 @@
+#!/bin/bash
+
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+set -e
+
+function info() {
+    echo -e "\033[32m$@\033[0m"
+}
+
+function warn() {
+    echo -e "\033[33m$@\033[0m"
+}
+
+function error() {
+    echo -e "\033[31m$@\033[0m"
+}
+
+function logging() {
+    case $1 in
+        "info") shift; info $@ ;;
+        "warn") shift; warn $@ ;;
+        "error") shift; error $@ ;;
+        *) echo -e $@ ;;
+    esac
+}
+
+if [[ -z  "$KYLIN_TPCH_HOME" ]]; then
+  dir=$(cd -P -- "$(dirname -- "$0")" && pwd -P)
+  export KYLIN_TPCH_HOME=`cd "${dir}/../"; pwd`
+  logging info "KYLIN_TPCH_HOME not set, will use $KYLIN_TPCH_HOME as KYLIN_TPCH_HOME."
+fi
+
+function check_python_version {
+  pyv=$(python3 --version 2>&1)
+  currentver=${pyv:7}
+  logging info "Current Python version is $currentver."
+
+  requiredver=3.6.6
+
+  if [[ "$(printf '%s\n' "$requiredver" "$currentver" | sort -V | head -n1)" != "$requiredver" ]]; then
+    logging error "${currentver} less than required Python version ${requiredver}."
+    exit 0
+  fi
+}
+
+function install_env {
+  check_python_version
+  if [[ ! -d $KYLIN_TPCH_HOME/venv ]]; then
+    python3 -m venv $KYLIN_TPCH_HOME/venv
+    source $KYLIN_TPCH_HOME/venv/bin/activate
+    logging info "Install dependencies ..."
+    pip3 install -r $KYLIN_TPCH_HOME/requirements.txt
+  else
+    logging warn "$KYLIN_TPCH_HOME/.venv already existing, skip install again."
+  fi
+  logging info "Please use 'source venv/bin/activate' to activate venv and execute commands."
+  logging info "Please use 'python ./deploy.py --h[|--help]' to check the usage."
+  logging info "Enjoy it and have fun."
+}
+
+function main {
+    install_env
+}
+
+main
diff --git a/client/__init__.py b/client/__init__.py
new file mode 100644
index 0000000..cce3aca
--- /dev/null
+++ b/client/__init__.py
@@ -0,0 +1,16 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
diff --git a/client/basic.py b/client/basic.py
new file mode 100644
index 0000000..84bfdcb
--- /dev/null
+++ b/client/basic.py
@@ -0,0 +1,133 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import logging
+import time
+
+import requests
+from retrying import retry
+
+
+logger = logging.getLogger(__name__)
+
+
+def _result(exception):
+    return isinstance(exception, requests.exceptions.ConnectTimeout)
+
+
+class BasicHttpClient:
+    _host = None
+    _port = None
+
+    _headers = {}
+
+    _auth = ('ADMIN', 'KYLIN')
+
+    _inner_session = requests.Session()
+
+    def __init__(self, host, port):
+        if not host or not port:
+            raise ValueError('init http client failed')
+
+        self._host = host
+        self._port = port
+
+    def token(self, token):
+        self._headers['Authorization'] = 'Basic {token}'.format(token=token)
+
+    def auth(self, username, password):
+        self._auth = (username, password)
+
+    def header(self, name, value):
+        self._headers[name] = value
+
+    def headers(self, headers):
+        self._headers = headers
+
+    def _request(self, method, url, params=None, data=None, json=None,  # pylint: disable=too-many-arguments
+                 files=None, headers=None, stream=False, to_json=True, inner_session=False, timeout=60, content=False,
+                 raw_response=False):
+        if inner_session:
+            return self._request_with_session(self._inner_session, method, url,
+                                              params=params,
+                                              data=data,
+                                              json=json,
+                                              files=files,
+                                              headers=headers,
+                                              stream=stream,
+                                              to_json=to_json,
+                                              timeout=timeout,
+                                              content=content,
+                                              raw_response=raw_response
+                                              )
+        with requests.Session() as session:
+            session.auth = self._auth
+            return self._request_with_session(session, method, url,
+                                              params=params,
+                                              data=data,
+                                              json=json,
+                                              files=files,
+                                              headers=headers,
+                                              stream=stream,
+                                              to_json=to_json,
+                                              timeout=timeout,
+                                              content=content,
+                                              raw_response=raw_response
+                                              )
+
+    @retry(stop_max_attempt_number=3, wait_random_min=100, wait_random_max=200, retry_on_exception=_result)
+    def _request_with_session(self, session, method, url, params=None, data=None,  # pylint: disable=too-many-arguments
+                              json=None, files=None, headers=None, stream=False,
+                              to_json=True, timeout=60, content=False, raw_response=False):
+        if headers is None:
+            headers = self._headers
+        resp = session.request(method, url,
+                               params=params,
+                               data=data,
+                               json=json,
+                               files=files,
+                               headers=headers,
+                               stream=stream,
+                               timeout=timeout,
+                               verify=False
+                               )
+
+        try:
+            if raw_response:
+                return resp
+            if stream:
+                return resp.raw
+            if not resp.content:
+                return None
+            if content:
+                return resp.content
+            if to_json:
+                data = resp.json()
+                resp.raise_for_status()
+                return data.get('data', data)
+            return resp.text
+        except requests.exceptions.ReadTimeout as timeout_error:
+            logger.error(timeout_error)
+            time.sleep(60)
+        except requests.HTTPError as http_error:
+            err_msg = f"{str(http_error)} [return code: {data.get('code', '')}]-[{data.get('msg', '')}]\n" \
+                      f"{data.get('stacktrace', '')}"
+            logger.error(err_msg)
+            raise requests.HTTPError(err_msg, request=http_error.request, response=http_error.response, )
+        except Exception as error:
+            logger.error(str(error))
+            raise error
diff --git a/client/lightning.py b/client/lightning.py
new file mode 100644
index 0000000..52d7181
--- /dev/null
+++ b/client/lightning.py
@@ -0,0 +1,116 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import logging
+import time
+
+from client.basic import BasicHttpClient
+
+logger = logging.getLogger(__name__)
+
+
+class LightningHttpClient(BasicHttpClient):
+    _base_url = 'http://{host}:{port}'
+
+    def __init__(self, host, port):
+        super().__init__(host, port)
+        self._headers = {
+            'Accept': 'application/json',
+            'Accept-Language': 'en-US',
+            'Content-Type': 'application/json;charset=utf-8'
+        }
+        self._base_url = self._base_url.format(host=self._host, port=self._port)
+
+    def set_headers(self, key, value=None):
+        if value is not None:
+            self._headers[key] = value
+        else:
+            if key in self._headers.keys():
+                self._headers.pop(key)
+
+    def set_base_url(self, url):
+        self._base_url = url
+
+    def await_kylin_start(self, check_action=None, timeout=600, check_times=30, **kwargs):
+        start_time = time.time()
+        already_check_times = 0
+        while time.time() - start_time < timeout:
+            try:
+                while already_check_times < check_times:
+                    res = check_action(**kwargs)
+                    assert res
+                    already_check_times = already_check_times + 1
+                    logger.info(f'Already check {already_check_times} times')
+                    time.sleep(1)
+                return True
+            except Exception as e:
+                logger.debug('Kylin can not access now {}, wait 10s'.format(e))
+                already_check_times = 0
+                time.sleep(10)
+        logger.debug('Kylin can not access after {}s'.format(timeout))
+        return False
+
+    def login(self, username, password, user_session=False):
+        """
+        use username and password login
+        :param username: string, target group name
+        :param password: array, the users add to group
+        :param user_session: boolean, true for using login session to execute
+        :return:
+        """
+        url = '/kylin/api/user/authentication'
+        self.auth(username, password)
+        resp = self._request('POST', url, inner_session=user_session)
+        return resp
+
+    def get_session(self):
+        return self._inner_session
+
+    def check_login_state(self):
+        url = '/kylin/api/user/authentication'
+        resp = self._request('GET', url, inner_session=False)
+        return resp
+
+    def execute_query(self, project, sql, offset=0, limit=500, accept_partial=False, user_session=False, timeout=360):
+        """
+        :param project: string, the name of project
+        :param sql: query sql
+        :param offset: offset of returned result
+        :param limit: limit of returned result
+        :param timeout: session timeout time
+        :return:
+        """
+        url = '/kylin/api/query'
+        payload = {
+            'limit': limit,
+            'offset': offset,
+            'project': project,
+            'acceptPartial': accept_partial,
+            'sql': sql,
+        }
+        resp = self._request('POST', url, json=payload, inner_session=user_session, timeout=timeout)
+        return resp
+
+    def _request(self, method, url, **kwargs):  # pylint: disable=arguments-differ
+        return super()._request(method, self._base_url + url, **kwargs)
+
+
+def connect(**conf):
+    _host = conf.get('host')
+    _port = conf.get('port')
+
+    return LightningHttpClient(_host, _port)
diff --git a/cloudformation_templates/ec2-cluster-kylin4-template.yaml b/cloudformation_templates/ec2-cluster-kylin4-template.yaml
new file mode 100644
index 0000000..5b77e12
--- /dev/null
+++ b/cloudformation_templates/ec2-cluster-kylin4-template.yaml
@@ -0,0 +1,252 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+
+AWSTemplateFormatVersion: 2010-09-09
+Description: Create Kylin4 nodes for ec2 instances and deploy related services, hadoop/spark/zookeeper/hive/jdk/kylin4
+Parameters:
+  # Must passed Parameters
+  DbHost:
+    Type: String
+    Description: Must be created at pre-step, db from ec2 instance
+  DbUser:
+    Type: String
+    Description: Db user for kylin4 to access RDS
+    Default: root
+  DbPassword:
+    Type: String
+    Description: Db password for kylin4 to access RDS
+  DbPort:
+    Type: String
+    Description: Db port for kylin4 to access RDS
+    Default: 3306
+  ZookeepersHost:
+    Type: String
+    Description: Must be created at pre-step
+  SparkMasterNodeHost:
+    Type: String
+    Description: Spark Master host for kylin 4
+  InstanceProfileId:
+    Type: String
+    Description: Must be created at pre-step
+  SubnetId:
+    Type: String
+  SecurityGroupId:
+    Type: String
+  AssociatedPublicIp:
+    Type: String
+    Default: true
+    AllowedValues:
+      - true
+      - false
+  EMREc2KeyName:
+    Type: String
+
+  ClusterNum:
+    Type: String
+    Default: default
+
+  IsScaled:
+    Type: String
+    Default: false
+    AllowedValues:
+      - true
+      - false
+
+  Ec2Mode:
+    Type: String
+    Default: test
+    AllowedValues:
+      - test
+      - product
+    Description: set the flag for Mode
+
+#   Optional Parameters
+  LocalCacheSoftAffinity:
+    Type: String
+    Default: false
+    AllowedValues:
+      - true
+      - false
+  BucketFullPath:
+    Type: String
+  BucketPath:
+    Type: String
+    Description: Url without prefix s3:/
+  Kylin4ScriptFileName:
+    Type: String
+    Default: prepare-ec2-env-for-kylin4.sh
+  InstanceType:
+    Description: EC2 instance type
+    Type: String
+    ConstraintDescription: must be a valid EC2 instance type.
+    Default: m5.2xlarge
+    AllowedValues:
+      - m5.xlarge
+      - m5.2xlarge
+      - m5.4xlarge
+  InstanceTypeForTest:
+    Description: EC2 instance type
+    Type: String
+    ConstraintDescription: must be a valid EC2 instance type.
+    Default: m5.2xlarge
+    AllowedValues:
+      - m5.2xlarge
+  Ec2VolumnTypeForKylin4Node:
+    Type: String
+    Default: gp2
+    AllowedValues:
+      - gp2
+      - gp3
+      - io1
+      - io2
+      - sc1
+      - st1
+      - standard
+  Ec2VolumeSizeForKylin4Node:
+    Type: Number
+    Default: 30
+    MinValue: 30
+    MaxValue: 100
+
+  Ec2VolumnTypeForKylin4NodeForTest:
+    Type: String
+    Default: gp2
+    AllowedValues:
+      - gp2
+
+  Ec2VolumeSizeForKylin4NodeForTest:
+    Type: Number
+    Default: 30
+    MinValue: 30
+    MaxValue: 30
+
+
+Mappings:
+  AWSRegionArch2AMI:
+    cn-north-1:
+      HVMebs: ami-00ac27054b887ff5c
+    cn-northwest-1:
+      HVMebs: ami-0135cb179d33fbe3e
+
+Conditions:
+  NotNullSubnetId:
+    !Not [!Equals [!Ref SubnetId, ""]]
+  NotNullEc2KeyName:
+    !Not [!Equals [!Ref EMREc2KeyName, ""]]
+  NotNullDbHost: !Not [!Equals [!Ref DbHost, ""]]
+  NotNullDbPass: !Not [!Equals [!Ref DbPassword, ""]]
+  NotNullDbUser: !Not [!Equals [!Ref DbUser, ""]]
+  NotNullZkHost: !Not [!Equals [!Ref ZookeepersHost, ""]]
+  NotNullSparkMasterHost: !Not [!Equals [!Ref SparkMasterNodeHost, ""]]
+  IsProductMode: !Equals [!Ref Ec2Mode, "product"]
+  ValidConfigurationForEc2: !And
+    - !Condition NotNullSubnetId
+    - !Condition NotNullEc2KeyName
+    - !Condition NotNullDbHost
+    - !Condition NotNullDbPass
+    - !Condition NotNullDbUser
+    - !Condition NotNullZkHost
+    - !Condition NotNullSparkMasterHost
+  IsAssociatedPublicIp: !Equals [!Ref AssociatedPublicIp, "true"]
+
+Resources:
+  Ec2InstanceOfKylin4:
+    Type: AWS::EC2::Instance
+    DeletionPolicy: Delete
+    Condition: ValidConfigurationForEc2
+    Properties:
+      ImageId: !FindInMap
+        - AWSRegionArch2AMI
+        - !Ref 'AWS::Region'
+        - HVMebs
+      Tags:
+        - Key: Project
+          Value: Kylin4
+        - Key: Name
+          Value: Kylin4 Node for kylin4
+      InstanceType:
+        !If
+          - IsProductMode
+          - !Ref InstanceType
+          - !Ref InstanceTypeForTest
+      IamInstanceProfile: !Ref InstanceProfileId
+      NetworkInterfaces:
+        - DeviceIndex: 0
+          Description: Auto create for Kylin4 node
+          DeleteOnTermination: true
+          AssociatePublicIpAddress: !Ref AssociatedPublicIp
+          SubnetId: !Ref SubnetId
+          GroupSet:
+            - !Ref SecurityGroupId
+      BlockDeviceMappings:
+        - !If
+          - IsProductMode
+          - DeviceName: /dev/xvda
+            Ebs:
+              VolumeSize: !Ref Ec2VolumeSizeForKylin4Node
+              VolumeType: !Ref Ec2VolumnTypeForKylin4Node
+              DeleteOnTermination: true
+          - DeviceName: /dev/xvda
+            Ebs:
+              VolumeSize: !Ref Ec2VolumeSizeForKylin4NodeForTest
+              VolumeType: !Ref Ec2VolumnTypeForKylin4NodeForTest
+              DeleteOnTermination: true
+      KeyName: !Ref EMREc2KeyName
+      UserData:
+        Fn::Base64:
+          Fn::Sub:
+            - |
+              #!/bin/bash -xe
+              cd /home/ec2-user
+              aws s3 cp ${PrivateBucketFullPath}/scripts/${PrivateKylin4ScriptFileName} . --region ${PrivateRegion}
+              bash ${PrivateKylin4ScriptFileName} --bucket-url ${PrivateBucketPath} --region ${PrivateRegion} --db-host ${PrivateDbHost} --db-password ${PrivateDbPass} --db-user ${PrivateDbUser} --db-port ${PrivateDbPort} --local-soft ${PrivateLocalCacheSoftAffinity} --cluster-num ${PrivateClusterNum} --is-scaled ${PrivateIsScaled}
+              echo " Kylin4 is ready ..."
+            - PrivateBucketFullPath: !Ref BucketFullPath
+              PrivateKylin4ScriptFileName: !Ref Kylin4ScriptFileName
+              PrivateBucketPath: !Ref BucketPath
+              PrivateRegion: !Ref AWS::Region
+              PrivateDbHost: !Ref DbHost
+              PrivateDbPass: !Ref DbPassword
+              PrivateDbUser: !Ref DbUser
+              PrivateDbPort: !Ref DbPort
+              PrivateLocalCacheSoftAffinity: !Ref LocalCacheSoftAffinity
+              PrivateClusterNum: !Ref ClusterNum
+              PrivateIsScaled: !Ref IsScaled
+
+Outputs:
+  IdOfInstance:
+    Description: the id of Kylin4 Node Innstance
+    Value: !Ref Ec2InstanceOfKylin4
+  Kylin4Ec2InstancePrivateIp:
+    Description: Kylin4 Instance Private IP
+    Value: !GetAtt Ec2InstanceOfKylin4.PrivateIp
+  Kylin4Ec2InstancePublicIp:
+    Description: Kylin4 Instance Public IP to acess
+    Value: !GetAtt Ec2InstanceOfKylin4.PublicIp
+    Condition: IsAssociatedPublicIp
+  Kylin4SubnetIdDependsOnDNode:
+    Description: SubnetId in the vpc for Slave Node
+    Value: !Ref SubnetId
+  Kylin4SecurityGroupIdDependsOnDNode:
+    Value: !Ref SecurityGroupId
+  Kylin4SparkMasterHost:
+    Description: Spark Master host for Kylin 4
+    Value: !Ref SparkMasterNodeHost
+  Kylin4ZookeeperHosts:
+    Description: Zookeeper hosts for Kylin 4
+    Value: !Ref ZookeepersHost
diff --git a/cloudformation_templates/ec2-cluster-kylin4.yaml b/cloudformation_templates/ec2-cluster-kylin4.yaml
new file mode 100644
index 0000000..dcfd775
--- /dev/null
+++ b/cloudformation_templates/ec2-cluster-kylin4.yaml
@@ -0,0 +1,251 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+AWSTemplateFormatVersion: 2010-09-09
+Description: Create Kylin4 nodes for ec2 instances and deploy related services, hadoop/spark/zookeeper/hive/jdk/kylin4
+Parameters:
+  # Must passed Parameters
+  DbHost:
+    Type: String
+    Description: Must be created at pre-step, db from ec2 instance
+  DbUser:
+    Type: String
+    Description: Db user for kylin4 to access RDS
+    Default: root
+  DbPassword:
+    Type: String
+    Description: Db password for kylin4 to access RDS
+  DbPort:
+    Type: String
+    Description: Db port for kylin4 to access RDS
+    Default: 3306
+  ZookeepersHost:
+    Type: String
+    Description: Must be created at pre-step
+  SparkMasterNodeHost:
+    Type: String
+    Description: Spark Master host for kylin 4
+  InstanceProfileId:
+    Type: String
+    Description: Must be created at pre-step
+  SubnetId:
+    Type: String
+  SecurityGroupId:
+    Type: String
+  AssociatedPublicIp:
+    Type: String
+    Default: true
+    AllowedValues:
+      - true
+      - false
+  EMREc2KeyName:
+    Type: String
+
+  ClusterNum:
+    Type: String
+    Default: default
+
+  IsScaled:
+    Type: String
+    Default: false
+    AllowedValues:
+      - true
+      - false
+
+  Ec2Mode:
+    Type: String
+    Default: test
+    AllowedValues:
+      - test
+      - product
+    Description: set the flag for Mode, if not test then use product's configuration
+
+#   Optional Parameters
+  LocalCacheSoftAffinity:
+    Type: String
+    Default: false
+    AllowedValues:
+      - true
+      - false
+  BucketFullPath:
+    Type: String
+  BucketPath:
+    Type: String
+    Description: Url without prefix s3:/
+  Kylin4ScriptFileName:
+    Type: String
+    Default: prepare-ec2-env-for-kylin4.sh
+  InstanceType:
+    Description: EC2 instance type
+    Type: String
+    ConstraintDescription: must be a valid EC2 instance type.
+    Default: m5.2xlarge
+    AllowedValues:
+      - m5.xlarge
+      - m5.2xlarge
+      - m5.4xlarge
+  InstanceTypeForTest:
+    Description: EC2 instance type
+    Type: String
+    ConstraintDescription: must be a valid EC2 instance type.
+    Default: m5.2xlarge
+    AllowedValues:
+      - m5.2xlarge
+  Ec2VolumnTypeForKylin4Node:
+    Type: String
+    Default: gp2
+    AllowedValues:
+      - gp2
+      - gp3
+      - io1
+      - io2
+      - sc1
+      - st1
+      - standard
+  Ec2VolumeSizeForKylin4Node:
+    Type: Number
+    Default: 30
+    MinValue: 30
+    MaxValue: 100
+
+  Ec2VolumnTypeForKylin4NodeForTest:
+    Type: String
+    Default: gp2
+    AllowedValues:
+      - gp2
+
+  Ec2VolumeSizeForKylin4NodeForTest:
+    Type: Number
+    Default: 30
+    MinValue: 30
+    MaxValue: 30
+
+
+Mappings:
+  AWSRegionArch2AMI:
+    cn-north-1:
+      HVMebs: ami-00ac27054b887ff5c
+    cn-northwest-1:
+      HVMebs: ami-0135cb179d33fbe3e
+
+Conditions:
+  NotNullSubnetId:
+    !Not [!Equals [!Ref SubnetId, ""]]
+  NotNullEc2KeyName:
+    !Not [!Equals [!Ref EMREc2KeyName, ""]]
+  NotNullDbHost: !Not [!Equals [!Ref DbHost, ""]]
+  NotNullDbPass: !Not [!Equals [!Ref DbPassword, ""]]
+  NotNullDbUser: !Not [!Equals [!Ref DbUser, ""]]
+  NotNullZkHost: !Not [!Equals [!Ref ZookeepersHost, ""]]
+  NotNullSparkMasterHost: !Not [!Equals [!Ref SparkMasterNodeHost, ""]]
+  IsProductMode: !Equals [!Ref Ec2Mode, "product"]
+  ValidConfigurationForEc2: !And
+    - !Condition NotNullSubnetId
+    - !Condition NotNullEc2KeyName
+    - !Condition NotNullDbHost
+    - !Condition NotNullDbPass
+    - !Condition NotNullDbUser
+    - !Condition NotNullZkHost
+    - !Condition NotNullSparkMasterHost
+  IsAssociatedPublicIp: !Equals [!Ref AssociatedPublicIp, "true"]
+
+Resources:
+  Ec2InstanceOfKylin4:
+    Type: AWS::EC2::Instance
+    DeletionPolicy: Delete
+    Condition: ValidConfigurationForEc2
+    Properties:
+      ImageId: !FindInMap
+        - AWSRegionArch2AMI
+        - !Ref 'AWS::Region'
+        - HVMebs
+      Tags:
+        - Key: Project
+          Value: Kylin4
+        - Key: Name
+          Value: Kylin4 Node for kylin4
+      InstanceType:
+        !If
+          - IsProductMode
+          - !Ref InstanceType
+          - !Ref InstanceTypeForTest
+      IamInstanceProfile: !Ref InstanceProfileId
+      NetworkInterfaces:
+        - DeviceIndex: 0
+          Description: Auto create for Kylin4 node
+          DeleteOnTermination: true
+          AssociatePublicIpAddress: !Ref AssociatedPublicIp
+          SubnetId: !Ref SubnetId
+          GroupSet:
+            - !Ref SecurityGroupId
+      BlockDeviceMappings:
+        - !If
+          - IsProductMode
+          - DeviceName: /dev/xvda
+            Ebs:
+              VolumeSize: !Ref Ec2VolumeSizeForKylin4Node
+              VolumeType: !Ref Ec2VolumnTypeForKylin4Node
+              DeleteOnTermination: true
+          - DeviceName: /dev/xvda
+            Ebs:
+              VolumeSize: !Ref Ec2VolumeSizeForKylin4NodeForTest
+              VolumeType: !Ref Ec2VolumnTypeForKylin4NodeForTest
+              DeleteOnTermination: true
+      KeyName: !Ref EMREc2KeyName
+      UserData:
+        Fn::Base64:
+          Fn::Sub:
+            - |
+              #!/bin/bash -xe
+              cd /home/ec2-user
+              aws s3 cp ${PrivateBucketFullPath}/scripts/${PrivateKylin4ScriptFileName} . --region ${PrivateRegion}
+              bash ${PrivateKylin4ScriptFileName} --bucket-url ${PrivateBucketPath} --region ${PrivateRegion} --db-host ${PrivateDbHost} --db-password ${PrivateDbPass} --db-user ${PrivateDbUser} --db-port ${PrivateDbPort} --local-soft ${PrivateLocalCacheSoftAffinity} --cluster-num ${PrivateClusterNum} --is-scaled ${PrivateIsScaled}
+              echo " Kylin4 is ready ..."
+            - PrivateBucketFullPath: !Ref BucketFullPath
+              PrivateKylin4ScriptFileName: !Ref Kylin4ScriptFileName
+              PrivateBucketPath: !Ref BucketPath
+              PrivateRegion: !Ref AWS::Region
+              PrivateDbHost: !Ref DbHost
+              PrivateDbPass: !Ref DbPassword
+              PrivateDbUser: !Ref DbUser
+              PrivateDbPort: !Ref DbPort
+              PrivateLocalCacheSoftAffinity: !Ref LocalCacheSoftAffinity
+              PrivateClusterNum: !Ref ClusterNum
+              PrivateIsScaled: !Ref IsScaled
+
+Outputs:
+  IdOfInstance:
+    Description: the id of Kylin4 Node Innstance
+    Value: !Ref Ec2InstanceOfKylin4
+  Kylin4Ec2InstancePrivateIp:
+    Description: Kylin4 Instance Private IP
+    Value: !GetAtt Ec2InstanceOfKylin4.PrivateIp
+  Kylin4Ec2InstancePublicIp:
+    Description: Kylin4 Instance Public IP to acess
+    Value: !GetAtt Ec2InstanceOfKylin4.PublicIp
+    Condition: IsAssociatedPublicIp
+  Kylin4SubnetIdDependsOnDNode:
+    Description: SubnetId in the vpc for Slave Node
+    Value: !Ref SubnetId
+  Kylin4SecurityGroupIdDependsOnDNode:
+    Value: !Ref SecurityGroupId
+  Kylin4SparkMasterHost:
+    Description: Spark Master host for Kylin 4
+    Value: !Ref SparkMasterNodeHost
+  Kylin4ZookeeperHosts:
+    Description: Zookeeper hosts for Kylin 4
+    Value: !Ref ZookeepersHost
diff --git a/cloudformation_templates/ec2-cluster-rds.yaml b/cloudformation_templates/ec2-cluster-rds.yaml
new file mode 100644
index 0000000..24ea418
--- /dev/null
+++ b/cloudformation_templates/ec2-cluster-rds.yaml
@@ -0,0 +1,149 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+AWSTemplateFormatVersion: 2010-09-09
+Description: Create Kylin4 nodes for ec2 instances and deploy related services, hadoop/spark/zookeeper/hive/jdk/kylin4
+Parameters:
+  # Must passed Parameters
+  Ec2DbIdentifier:
+    Default: ec2-rds-kylin4
+    Type: String
+
+  Ec2DbUser:
+    NoEcho: 'true'
+    Description: Username for MySQL database access
+    Type: String
+    MinLength: '1'
+    MaxLength: '16'
+    AllowedPattern: '[a-zA-Z][a-zA-Z0-9]*'
+    Default: root
+  Ec2DbPassword:
+    NoEcho: 'true'
+    Type: String
+    MinLength: '8'
+    MaxLength: '41'
+    AllowedPattern: '[a-zA-Z0-9]*'
+    ConstraintDescription: must contain only alphanumeric characters.
+    Description: Must be created at pre-step and this can be modified in the script not yaml file.
+  Ec2DbPort:
+    Type: String
+    Default: 3306
+    AllowedValues:
+      - 3306
+
+  SecurityGroupId:
+    Type: String
+  SubnetGroupName:
+    Type: String
+
+  RDSEngine:
+    Type: String
+    Default: mysql
+    AllowedValues:
+      - mysql
+  RDSEngineVersion:
+    Type: String
+    Default: 5.7.25
+
+  Ec2Mode:
+    Type: String
+    Default: test
+    AllowedValues:
+      - test
+      - product
+    Description: set the flag for Mode, if not `test` then use `product`
+
+#   Optional Parameters
+  EnableIAMAuthentication:
+    Type: String
+    Default: false
+    AllowedValues:
+      - true
+      - false
+  Zone:
+    Type: String
+    Default: cn-northwest-1b
+    AllowedValues:
+      - cn-northwest-1a
+      - cn-northwest-1b
+  DbName:
+    Type: String
+    Description: Initilize a db in rds
+    Default: kylin
+    AllowedValues:
+      - kylin
+  TypeOfRDS:
+    Description: RDS instance type
+    Type: String
+    ConstraintDescription: must be a valid EC2 instance type.
+    Default: db.t3.micro
+    AllowedValues:
+      - db.t3.micro
+      - db.m5.xlarge
+  VolumnTypeForRDS:
+    Type: String
+    Default: gp2
+    AllowedValues:
+      - gp2
+  VolumeSizeForRDS:
+    Type: Number
+    Default: 10
+    MinValue: 5
+    MaxValue: 20
+
+Conditions:
+  NotNullSecurityGroupID: !Not [!Equals [!Ref SecurityGroupId, ""]]
+  IsProductMode: !Equals [!Ref Ec2Mode, "product"]
+
+Resources:
+  Ec2RDSInstance:
+    Type: AWS::RDS::DBInstance
+    DeletionPolicy: Delete
+    Condition: NotNullSecurityGroupID
+    Properties:
+      Tags:
+        - Key: Project
+          Value: Kylin4
+        - Key: Name
+          Value: RDS for kylin4
+      AllocatedStorage: !Ref VolumeSizeForRDS
+      AvailabilityZone: !Ref Zone
+      DBSubnetGroupName: !Ref SubnetGroupName
+      # The number of days for which automated backups are retained. Setting this parameter to a positive number enables backups.
+      # Setting this parameter to 0 disables automated backups.
+      BackupRetentionPeriod: 0
+      DBInstanceClass: !Ref TypeOfRDS
+      DBInstanceIdentifier: !Ref Ec2DbIdentifier
+      DBName: !Ref DbName
+      VPCSecurityGroups:
+        - !Ref SecurityGroupId
+      EnableIAMDatabaseAuthentication: !Ref EnableIAMAuthentication
+      Engine: !Ref RDSEngine
+      EngineVersion: !Ref RDSEngineVersion
+      MasterUsername: !Ref Ec2DbUser
+      MasterUserPassword: !Ref Ec2DbPassword
+      Port: !Ref Ec2DbPort
+      StorageType: !Ref VolumnTypeForRDS
+
+Outputs:
+  DbHost:
+    Description: Db Endpoints Host
+    Value: !GetAtt Ec2RDSInstance.Endpoint.Address
+  DbPort:
+    Description: Db Endpoints Port
+    Value: !GetAtt Ec2RDSInstance.Endpoint.Port
+
diff --git a/cloudformation_templates/ec2-cluster-spark-master.yaml b/cloudformation_templates/ec2-cluster-spark-master.yaml
new file mode 100644
index 0000000..dfd5a8b
--- /dev/null
+++ b/cloudformation_templates/ec2-cluster-spark-master.yaml
@@ -0,0 +1,219 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+AWSTemplateFormatVersion: 2010-09-09
+Description: Create SparkMaster nodes for ec2 instances and deploy related services, hadoop/spark/zookeeper/hive/jdk/kylin4
+Parameters:
+  # Must passed Parameters
+  InstanceProfileId:
+    Type: String
+    Description: Must be created at pre-step
+  SubnetId:
+    Type: String
+  SecurityGroupId:
+    Type: String
+  AssociatedPublicIp:
+    Type: String
+    Default: true
+    AllowedValues:
+      - true
+      - false
+  EMREc2KeyName:
+    Type: String
+  DbHost:
+    Type: String
+    Description: the host of RDS instance
+  DbUser:
+    Type: String
+    Description: the user to access RDS instance
+  DbPassword:
+    Type: String
+    Description: the password to access RDS instance
+  DbPort:
+    Type: String
+    Description: the port to access RDS instance
+    Default: 3306
+
+  Ec2Mode:
+    Type: String
+    Default: test
+    AllowedValues:
+      - test
+      - product
+    Description: set the flag for Mode, if not test use product's configuration
+
+#   Optional Parameters
+  LocalCacheSoftAffinity:
+    Type: String
+    Default: false
+    AllowedValues:
+      - true
+      - false
+  BucketFullPath:
+    Type: String
+  BucketPath:
+    Type: String
+    Description: Url without prefix s3:/
+  SparkMasterScriptFileName:
+    Type: String
+    Default: prepare-ec2-env-for-spark-master.sh
+  InstanceType:
+    Description: EC2 instance type
+    Type: String
+    ConstraintDescription: must be a valid EC2 instance type.
+    Default: m5.2xlarge
+    AllowedValues:
+      - m5.xlarge
+      - m5.2xlarge
+      - m5.4xlarge
+  InstanceTypeForTest:
+    Description: EC2 instance type
+    Type: String
+    ConstraintDescription: must be a valid EC2 instance type.
+    Default: m5.2xlarge
+    AllowedValues:
+      - m5.2xlarge
+  Ec2VolumnTypeForSparkMasterNode:
+    Type: String
+    Default: gp2
+    AllowedValues:
+      - gp2
+      - gp3
+      - io1
+      - io2
+      - sc1
+      - st1
+      - standard
+  Ec2VolumeSizeForSparkMasterNode:
+    Type: Number
+    Default: 30
+    MinValue: 30
+    MaxValue: 100
+
+  Ec2VolumnTypeForSparkMasterNodeForTest:
+    Type: String
+    Default: gp2
+    AllowedValues:
+      - gp2
+
+  Ec2VolumeSizeForSparkMasterNodeForTest:
+    Type: Number
+    Default: 30
+    MinValue: 30
+    MaxValue: 30
+
+
+Mappings:
+  AWSRegionArch2AMI:
+    cn-north-1:
+      HVMebs: ami-00ac27054b887ff5c
+    cn-northwest-1:
+      HVMebs: ami-0135cb179d33fbe3e
+
+Conditions:
+  NotNullSubnetId:
+    !Not [!Equals [!Ref SubnetId, ""]]
+  NotNullEc2KeyName:
+    !Not [!Equals [!Ref EMREc2KeyName, ""]]
+  IsProductMode: !Equals [!Ref Ec2Mode, "product"]
+  ValidConfigurationForEc2: !And
+    - !Condition NotNullSubnetId
+    - !Condition NotNullEc2KeyName
+  IsAssociatedPublicIp: !Equals [!Ref AssociatedPublicIp, "true"]
+
+Resources:
+  Ec2InstanceOfSparkMaster:
+    Type: AWS::EC2::Instance
+    DeletionPolicy: Delete
+    Condition: ValidConfigurationForEc2
+    Properties:
+      ImageId: !FindInMap
+        - AWSRegionArch2AMI
+        - !Ref 'AWS::Region'
+        - HVMebs
+      Tags:
+        - Key: Project
+          Value: Kylin4
+        - Key: Name
+          Value: Spark Master Node for kylin4
+      InstanceType:
+        !If
+          - IsProductMode
+          - !Ref InstanceType
+          - !Ref InstanceTypeForTest
+      IamInstanceProfile: !Ref InstanceProfileId
+      NetworkInterfaces:
+        - DeviceIndex: 0
+          Description: Auto create for SparkMaster node in benchmark
+          DeleteOnTermination: true
+          AssociatePublicIpAddress: !Ref AssociatedPublicIp
+          SubnetId: !Ref SubnetId
+          GroupSet:
+            - !Ref SecurityGroupId
+      BlockDeviceMappings:
+        - !If
+          - IsProductMode
+          - DeviceName: /dev/xvda
+            Ebs:
+              VolumeSize: !Ref Ec2VolumeSizeForSparkMasterNode
+              VolumeType: !Ref Ec2VolumnTypeForSparkMasterNode
+              DeleteOnTermination: true
+          - DeviceName: /dev/xvda
+            Ebs:
+              VolumeSize: !Ref Ec2VolumeSizeForSparkMasterNodeForTest
+              VolumeType: !Ref Ec2VolumnTypeForSparkMasterNodeForTest
+              DeleteOnTermination: true
+      KeyName: !Ref EMREc2KeyName
+      UserData:
+        Fn::Base64:
+          Fn::Sub:
+            - |
+              #!/bin/bash -xe
+              cd /home/ec2-user
+              aws s3 cp ${PrivateBucketFullPath}/scripts/${PrivateSparkMasterScriptFileName} . --region ${PrivateRegion}
+              bash ${PrivateSparkMasterScriptFileName} --bucket-url ${PrivateBucketPath} --region ${PrivateRegion} --local-soft ${PrivateLocalCacheSoftAffinity} --db-port ${PrivateDbPort} --db-host ${PrivateDbHost} --db-user ${PrivateDbUser} --db-password ${PrivateDbPassword}
+              echo " Spark Master is ready ..."
+            - PrivateBucketFullPath: !Ref BucketFullPath
+              PrivateSparkMasterScriptFileName: !Ref SparkMasterScriptFileName
+              PrivateBucketPath: !Ref BucketPath
+              PrivateRegion: !Ref AWS::Region
+              PrivateLocalCacheSoftAffinity: !Ref LocalCacheSoftAffinity
+              PrivateDbPort: !Ref DbPort
+              PrivateDbHost: !Ref DbHost
+              PrivateDbUser: !Ref DbUser
+              PrivateDbPassword: !Ref DbPassword
+
+Outputs:
+  IdOfInstance:
+    Description: the id of SparkMaster Node Instance
+    Value: !Ref Ec2InstanceOfSparkMaster
+  SparkMasterNodeHost:
+    Description: SparkMaster host is same as private IP
+    Value: !GetAtt Ec2InstanceOfSparkMaster.PrivateIp
+  SparkMasterEc2InstancePrivateIp:
+    Description: SparkMaster Instance Private IP
+    Value: !GetAtt Ec2InstanceOfSparkMaster.PrivateIp
+  SparkMasterEc2InstancePublicIp:
+    Description: SparkMaster Instance Public IP to acess
+    Value: !GetAtt Ec2InstanceOfSparkMaster.PublicIp
+    Condition: IsAssociatedPublicIp
+  SparkMasterEc2InstanceProfileId:
+    Value: !Ref InstanceProfileId
+  SparkMasterSubnetIdDependsOnDNode:
+    Value: !Ref SubnetId
+  SparkMasterSecurityGroupIdDependsOnDNode:
+    Value: !Ref SecurityGroupId
diff --git a/cloudformation_templates/ec2-cluster-spark-slave-template.yaml b/cloudformation_templates/ec2-cluster-spark-slave-template.yaml
new file mode 100644
index 0000000..0e6260c
--- /dev/null
+++ b/cloudformation_templates/ec2-cluster-spark-slave-template.yaml
@@ -0,0 +1,210 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+AWSTemplateFormatVersion: 2010-09-09
+Description: Create Other needed worker nodes for ec2 instances and deploy related services, hadoop/spark/zookeeper/hive/jdk/kylin4
+Parameters:
+  # Must passed Parameters
+  SparkMasterNodeHost:
+    Type: String
+    Description: Must be created at pre-step
+  InstanceProfileId:
+    Type: String
+    Description: Must be created at pre-step
+
+  SubnetId:
+    Type: String
+  SecurityGroupId:
+    Type: String
+  EMREc2KeyName:
+    Type: String
+
+  AssociatedPublicIp:
+    Type: String
+    Description: whether to associated public ip for instances
+    Default: false
+    AllowedValues:
+      - true
+      - false
+
+  WorkerNum:
+    Type: String
+    Description: mark woker number for spark worker
+
+  #   Optional Parameters
+  LocalCacheSoftAffinity:
+    Type: String
+    Default: false
+    AllowedValues:
+      - true
+      - false
+  Ec2Mode:
+    Type: String
+    Default: test
+    AllowedValues:
+      - test
+      - product
+  WaitingTime:
+    Type: Number
+    Default: 600
+    Description: Waiting time for start slave worker
+  BucketFullPath:
+    Type: String
+  BucketPath:
+    Type: String
+    Description: Url without prefix s3:/
+  ScriptFileName:
+    Type: String
+    Default: prepare-ec2-env-for-spark-slave.sh
+  InstanceType:
+    Description: EC2 instance type
+    Type: String
+    ConstraintDescription: Must be a valid EC2 instance type.
+    Default: m5.2xlarge
+    AllowedValues:
+      - m5.xlarge
+      - m5.2xlarge
+      - m5.4xlarge
+  Ec2VolumnTypeForSlaveNode:
+    Type: String
+    Default: gp2
+    AllowedValues:
+      - gp2
+      - gp3
+      - io1
+      - io2
+      - sc1
+      - st1
+      - standard
+  Ec2VolumeSizeForSlaveNode:
+    Type: Number
+    Default: 400
+    MinValue: 30
+    MaxValue: 400
+  Ec2VolumnTypeForSlaveNodeForTest:
+    Type: String
+    Default: gp2
+    AllowedValues:
+      - gp2
+  Ec2VolumeSizeForSlaveNodeForTest:
+    Type: Number
+    Default: 30
+    MinValue: 30
+    MaxValue: 30
+
+
+Mappings:
+  AWSRegionArch2AMI:
+    cn-north-1:
+      HVMebs: ami-00ac27054b887ff5c
+    cn-northwest-1:
+      HVMebs: ami-0135cb179d33fbe3e
+
+Conditions:
+  NotNullSubnetId:
+    !Not [!Equals [!Ref SubnetId, ""]]
+  NotNullEc2KeyName:
+    !Not [!Equals [!Ref EMREc2KeyName, ""]]
+  NotNullSparkMasterNodeHost: !Not [!Equals [!Ref SparkMasterNodeHost, ""]]
+  IsProductMode: !Equals [!Ref Ec2Mode, "product"]
+  ValidConfigurationForEc2: !And
+    - !Condition NotNullSubnetId
+    - !Condition NotNullEc2KeyName
+    - !Condition NotNullSparkMasterNodeHost
+  IsAssociatedPublicIp: !Equals [!Ref AssociatedPublicIp, "true"]
+
+Resources:
+  Ec2InstanceOfSlave:
+    Type: AWS::EC2::Instance
+    DeletionPolicy: Delete
+    Condition: ValidConfigurationForEc2
+    Description: Scale node WorkerNum
+    Properties:
+      ImageId: !FindInMap
+        - AWSRegionArch2AMI
+        - !Ref 'AWS::Region'
+        - HVMebs
+      Tags:
+        - Key: Project
+          Value: Kylin4
+        - Key: Name
+          Value: !Join
+            - ''
+            - - Slave Scale
+              - !Ref WorkerNum
+              - Node for kylin4
+      InstanceType: !Ref InstanceType
+      IamInstanceProfile: !Ref InstanceProfileId
+      NetworkInterfaces:
+        - DeviceIndex: 0
+          Description: Scale Slave Node NetworkInterfaces
+          DeleteOnTermination: true
+          AssociatePublicIpAddress: !Ref AssociatedPublicIp
+          SubnetId: !Ref SubnetId
+          GroupSet:
+            - !Ref SecurityGroupId
+      BlockDeviceMappings:
+        - !If
+          - IsProductMode
+          - DeviceName: /dev/xvda
+            Ebs:
+              VolumeSize: !Ref Ec2VolumeSizeForSlaveNode
+              VolumeType: !Ref Ec2VolumnTypeForSlaveNode
+              DeleteOnTermination: true
+          - DeviceName: /dev/xvda
+            Ebs:
+              VolumeSize: !Ref Ec2VolumeSizeForSlaveNodeForTest
+              VolumeType: !Ref Ec2VolumnTypeForSlaveNodeForTest
+              DeleteOnTermination: true
+        - !If
+          - IsProductMode
+          - DeviceName: /dev/xvdb
+            Ebs:
+              VolumeSize: !Ref Ec2VolumeSizeForSlaveNode
+              VolumeType: !Ref Ec2VolumnTypeForSlaveNode
+              DeleteOnTermination: true
+          - !Ref AWS::NoValue
+      KeyName: !Ref EMREc2KeyName
+      UserData:
+        Fn::Base64:
+          Fn::Sub:
+            - |
+              #!/bin/bash -xe
+              cd /home/ec2-user
+              aws s3 cp ${PrivateBucketFullPath}/scripts/${PrivateSlaveScriptFileName} . --region ${PrivateRegion}
+              bash ${PrivateSlaveScriptFileName} --bucket-url ${PrivateBucketPath} --master-host ${PrivateMasterHost} --worker-number ${WorkerNum} --region ${PrivateRegion} --waiting-time ${PrivateWaitingTime} --mode ${WorkerMode} --local-soft ${PrivateLocalCacheSoftAffinity}
+            - PrivateMasterHost: !Ref SparkMasterNodeHost
+              WorkerNum: !Ref WorkerNum
+              PrivateBucketFullPath: !Ref BucketFullPath
+              PrivateSlaveScriptFileName: !Ref ScriptFileName
+              PrivateBucketPath: !Ref BucketPath
+              PrivateRegion: !Ref AWS::Region
+              PrivateWaitingTime: !Ref WaitingTime
+              WorkerMode: !Ref Ec2Mode
+              PrivateLocalCacheSoftAffinity: !Ref LocalCacheSoftAffinity
+
+Outputs:
+  IdOfInstance:
+    Description: the id of Slave Instance
+    Value: !Ref Ec2InstanceOfSlave
+  SlaveEc2InstancePrivateIp:
+    Description: the Slave Instance Private IP
+    Value: !GetAtt Ec2InstanceOfSlave.PrivateIp
+  SlaveEc2InstancePublicIp:
+    Description: the Slave Instance Public IP
+    Value: !GetAtt Ec2InstanceOfSlave.PublicIp
+    Condition: IsAssociatedPublicIp
diff --git a/cloudformation_templates/ec2-cluster-spark-slave.yaml b/cloudformation_templates/ec2-cluster-spark-slave.yaml
new file mode 100644
index 0000000..ba5a923
--- /dev/null
+++ b/cloudformation_templates/ec2-cluster-spark-slave.yaml
@@ -0,0 +1,377 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+AWSTemplateFormatVersion: 2010-09-09
+Description: Create Slave nodes for ec2 instances and deploy related services, hadoop/spark/zookeeper/hive/jdk/kylin4
+Parameters:
+  # Must passed Parameters
+  SparkMasterNodeHost:
+    Type: String
+    Description: Must be created at pre-step
+  InstanceProfileId:
+    Type: String
+    Description: Must be created at pre-step
+
+  SubnetId:
+    Type: String
+  SecurityGroupId:
+    Type: String
+  EMREc2KeyName:
+    Type: String
+
+  AssociatedPublicIp:
+    Type: String
+    Description: whether to associated public ip for instances
+    Default: false
+    AllowedValues:
+      - true
+      - false
+
+#   Optional Parameters
+  LocalCacheSoftAffinity:
+    Type: String
+    Default: false
+    AllowedValues:
+      - true
+      - false
+  Ec2Mode:
+    Type: String
+    Default: test
+    AllowedValues:
+      - test
+      - product
+
+  WaitingTime:
+    Type: Number
+    Default: 50
+    Description: Waiting time for start slave worker
+  BucketFullPath:
+    Type: String
+    Description: example s3://xxx/kylin
+  BucketPath:
+    Type: String
+    Description: example /xxx/kylin which Url is without prefix s3:/
+  SlaveScriptFileName:
+    Type: String
+    Default: prepare-ec2-env-for-spark-slave.sh
+  InstanceType:
+    Description: EC2 instance type
+    Type: String
+    ConstraintDescription: must be a valid EC2 instance type.
+    Default: m5.2xlarge
+    AllowedValues:
+      - m5.xlarge
+      - m5.2xlarge
+      - m5.4xlarge
+  InstanceTypeForTest:
+    Description: EC2 instance type
+    Type: String
+    ConstraintDescription: must be a valid EC2 instance type.
+    Default: m5.2xlarge
+    AllowedValues:
+      - m5.2xlarge
+  Ec2VolumnTypeForSlaveNode:
+    Type: String
+    Default: gp2
+    AllowedValues:
+      - gp2
+      - gp3
+      - io1
+      - io2
+      - sc1
+      - st1
+      - standard
+  Ec2VolumeSizeForSlaveNode:
+    Type: Number
+    Default: 30
+    MinValue: 30
+    MaxValue: 400
+
+  Ec2VolumnTypeForSlaveNodeForTest:
+    Type: String
+    Default: gp2
+    AllowedValues:
+      - gp2
+  Ec2VolumeSizeForSlaveNodeForTest:
+    Type: Number
+    Default: 30
+    MinValue: 30
+    MaxValue: 30
+
+
+Mappings:
+  AWSRegionArch2AMI:
+    cn-north-1:
+      HVMebs: ami-00ac27054b887ff5c
+    cn-northwest-1:
+      HVMebs: ami-0135cb179d33fbe3e
+
+Conditions:
+  NotNullSubnetId:
+    !Not [!Equals [!Ref SubnetId, ""]]
+  NotNullEc2KeyName:
+    !Not [!Equals [!Ref EMREc2KeyName, ""]]
+  NotNullSparkMasterNodeHost: !Not [!Equals [!Ref SparkMasterNodeHost, ""]]
+  IsProductMode: !Equals [!Ref Ec2Mode, "product"]
+  ValidConfigurationForEc2: !And
+    - !Condition NotNullSubnetId
+    - !Condition NotNullEc2KeyName
+    - !Condition NotNullSparkMasterNodeHost
+  IsAssociatedPublicIp: !Equals [!Ref AssociatedPublicIp, "true"]
+
+Resources:
+  Ec2InstanceOfSlave01:
+    Type: AWS::EC2::Instance
+    DeletionPolicy: Delete
+    Condition: ValidConfigurationForEc2
+    Description: default slave01 node is created for query or job
+    Properties:
+      ImageId: !FindInMap
+        - AWSRegionArch2AMI
+        - !Ref 'AWS::Region'
+        - HVMebs
+      Tags:
+        - Key: Project
+          Value: Kylin4
+        - Key: Name
+          Value: Slave01 Node for kylin4
+      InstanceType:
+        !If
+          - IsProductMode
+          - !Ref InstanceType
+          - !Ref InstanceTypeForTest
+      IamInstanceProfile: !Ref InstanceProfileId
+      NetworkInterfaces:
+        - DeviceIndex: 0
+          Description: Auto create for Slave01 node in benchmark
+          DeleteOnTermination: true
+          AssociatePublicIpAddress: !Ref AssociatedPublicIp
+          SubnetId: !Ref SubnetId
+          GroupSet:
+            - !Ref SecurityGroupId
+      BlockDeviceMappings:
+        - !If
+          - IsProductMode
+          - DeviceName: /dev/xvda
+            Ebs:
+              VolumeSize: !Ref Ec2VolumeSizeForSlaveNode
+              VolumeType: !Ref Ec2VolumnTypeForSlaveNode
+              DeleteOnTermination: true
+          - DeviceName: /dev/xvda
+            Ebs:
+              VolumeSize: !Ref Ec2VolumeSizeForSlaveNodeForTest
+              VolumeType: !Ref Ec2VolumnTypeForSlaveNodeForTest
+              DeleteOnTermination: true
+        - !If
+          - IsProductMode
+          - DeviceName: /dev/xvdb
+            Ebs:
+              VolumeSize: !Ref Ec2VolumeSizeForSlaveNode
+              VolumeType: !Ref Ec2VolumnTypeForSlaveNode
+              DeleteOnTermination: true
+          - !Ref AWS::NoValue
+
+      KeyName: !Ref EMREc2KeyName
+      UserData:
+        Fn::Base64:
+          Fn::Sub:
+            - |
+              #!/bin/bash -xe
+              cd /home/ec2-user
+              aws s3 cp ${PrivateBucketFullPath}/scripts/${PrivateSlaveScriptFileName} . --region ${PrivateRegion}
+              bash ${PrivateSlaveScriptFileName} --bucket-url ${PrivateBucketPath} --master-host ${PrivateMasterHost} --worker-number ${WorkerNum} --region ${PrivateRegion} --waiting-time ${PrivateWaitingTime} --mode ${WorkerMode} --local-soft ${PrivateLocalCacheSoftAffinity}
+            - PrivateMasterHost: !Ref SparkMasterNodeHost
+              WorkerNum: 1
+              PrivateBucketFullPath: !Ref BucketFullPath
+              PrivateSlaveScriptFileName: !Ref SlaveScriptFileName
+              PrivateBucketPath: !Ref BucketPath
+              PrivateRegion: !Ref AWS::Region
+              PrivateWaitingTime: !Ref WaitingTime
+              WorkerMode: !Ref Ec2Mode
+              PrivateLocalCacheSoftAffinity: !Ref LocalCacheSoftAffinity
+  Ec2InstanceOfSlave02:
+    Type: AWS::EC2::Instance
+    DeletionPolicy: Delete
+    Condition: ValidConfigurationForEc2
+    Properties:
+      ImageId: !FindInMap
+        - AWSRegionArch2AMI
+        - !Ref 'AWS::Region'
+        - HVMebs
+      Tags:
+        - Key: Project
+          Value: Kylin4
+        - Key: Name
+          Value: Slave02 Node for Kylin4
+      InstanceType:
+        !If
+          - IsProductMode
+          - !Ref InstanceType
+          - !Ref InstanceTypeForTest
+      IamInstanceProfile: !Ref InstanceProfileId
+      NetworkInterfaces:
+        - DeviceIndex: 0
+          Description: Auto create for Slave02 node in benchmark
+          DeleteOnTermination: true
+          AssociatePublicIpAddress: !Ref AssociatedPublicIp
+          SubnetId: !Ref SubnetId
+          GroupSet:
+            - !Ref SecurityGroupId
+      BlockDeviceMappings:
+        - !If
+          - IsProductMode
+          - DeviceName: /dev/xvda
+            Ebs:
+              VolumeSize: !Ref Ec2VolumeSizeForSlaveNode
+              VolumeType: !Ref Ec2VolumnTypeForSlaveNode
+              DeleteOnTermination: true
+          - DeviceName: /dev/xvda
+            Ebs:
+              VolumeSize: !Ref Ec2VolumeSizeForSlaveNodeForTest
+              VolumeType: !Ref Ec2VolumnTypeForSlaveNodeForTest
+              DeleteOnTermination: true
+        - !If
+          - IsProductMode
+          - DeviceName: /dev/xvdb
+            Ebs:
+              VolumeSize: !Ref Ec2VolumeSizeForSlaveNode
+              VolumeType: !Ref Ec2VolumnTypeForSlaveNode
+              DeleteOnTermination: true
+          - !Ref AWS::NoValue
+
+      KeyName: !Ref EMREc2KeyName
+      UserData:
+        Fn::Base64:
+          Fn::Sub:
+            - |
+              #!/bin/bash -xe
+              cd /home/ec2-user
+              aws s3 cp ${PrivateBucketFullPath}/scripts/${PrivateSlaveScriptFileName} . --region ${PrivateRegion}
+              bash ${PrivateSlaveScriptFileName} --bucket-url ${PrivateBucketPath} --master-host ${PrivateMasterHost} --worker-number ${WorkerNum} --region ${PrivateRegion} --waiting-time ${PrivateWaitingTime} --mode ${WorkerMode} --local-soft ${PrivateLocalCacheSoftAffinity}
+            - PrivateMasterHost: !Ref SparkMasterNodeHost
+              WorkerNum: 2
+              PrivateBucketFullPath: !Ref BucketFullPath
+              PrivateSlaveScriptFileName: !Ref SlaveScriptFileName
+              PrivateBucketPath: !Ref BucketPath
+              PrivateRegion: !Ref AWS::Region
+              PrivateWaitingTime: !Ref WaitingTime
+              WorkerMode: !Ref Ec2Mode
+              PrivateLocalCacheSoftAffinity: !Ref LocalCacheSoftAffinity
+
+  Ec2InstanceOfSlave03:
+    Type: AWS::EC2::Instance
+    DeletionPolicy: Delete
+    Condition: ValidConfigurationForEc2
+    Properties:
+      ImageId: !FindInMap
+        - AWSRegionArch2AMI
+        - !Ref 'AWS::Region'
+        - HVMebs
+      Tags:
+        - Key: Project
+          Value: Kylin4
+        - Key: Name
+          Value: Slave03 Node for Kylin4
+      InstanceType:
+        !If
+          - IsProductMode
+          - !Ref InstanceType
+          - !Ref InstanceTypeForTest
+      IamInstanceProfile: !Ref InstanceProfileId
+      NetworkInterfaces:
+        - DeviceIndex: 0
+          Description: Auto create for Slave03 node in benchmark
+          DeleteOnTermination: true
+          AssociatePublicIpAddress: !Ref AssociatedPublicIp
+          SubnetId: !Ref SubnetId
+          GroupSet:
+            - !Ref SecurityGroupId
+      BlockDeviceMappings:
+        - !If
+          - IsProductMode
+          - DeviceName: /dev/xvda
+            Ebs:
+              VolumeSize: !Ref Ec2VolumeSizeForSlaveNode
+              VolumeType: !Ref Ec2VolumnTypeForSlaveNode
+              DeleteOnTermination: true
+          - DeviceName: /dev/xvda
+            Ebs:
+              VolumeSize: !Ref Ec2VolumeSizeForSlaveNodeForTest
+              VolumeType: !Ref Ec2VolumnTypeForSlaveNodeForTest
+              DeleteOnTermination: true
+        - !If
+          - IsProductMode
+          - DeviceName: /dev/xvdb
+            Ebs:
+              VolumeSize: !Ref Ec2VolumeSizeForSlaveNode
+              VolumeType: !Ref Ec2VolumnTypeForSlaveNode
+              DeleteOnTermination: true
+          - !Ref AWS::NoValue
+
+      KeyName: !Ref EMREc2KeyName
+      UserData:
+        Fn::Base64:
+          Fn::Sub:
+            - |
+              #!/bin/bash -xe
+              cd /home/ec2-user
+              aws s3 cp ${PrivateBucketFullPath}/scripts/${PrivateSlaveScriptFileName} . --region ${PrivateRegion}
+              bash ${PrivateSlaveScriptFileName} --bucket-url ${PrivateBucketPath} --master-host ${PrivateMasterHost} --worker-number ${WorkerNum} --region ${PrivateRegion} --waiting-time ${PrivateWaitingTime} --mode ${WorkerMode} --local-soft ${PrivateLocalCacheSoftAffinity}
+            - PrivateMasterHost: !Ref SparkMasterNodeHost
+              WorkerNum: 3
+              PrivateBucketFullPath: !Ref BucketFullPath
+              PrivateSlaveScriptFileName: !Ref SlaveScriptFileName
+              PrivateBucketPath: !Ref BucketPath
+              PrivateRegion: !Ref AWS::Region
+              PrivateWaitingTime: !Ref WaitingTime
+              WorkerMode: !Ref Ec2Mode
+              PrivateLocalCacheSoftAffinity: !Ref LocalCacheSoftAffinity
+
+
+Outputs:
+  IdOfInstanceSlave01:
+    Description: the id of Slave 01 Node Instance if created
+    Value: !Ref Ec2InstanceOfSlave01
+  Slave01Ec2InstancePrivateIp:
+    Description: Slave01 Instance Private IP
+    Value: !GetAtt Ec2InstanceOfSlave01.PrivateIp
+  Slave01Ec2InstancePublicIp:
+    Description: Slave01 Instance Public IP
+    Value: !GetAtt Ec2InstanceOfSlave01.PublicIp
+    Condition: IsAssociatedPublicIp
+
+  IdOfInstanceSlave02:
+    Description: the id of Slave 02 Node Instance if created
+    Value: !Ref Ec2InstanceOfSlave02
+  Slave02Ec2InstancePrivateIp:
+    Description: Slave02 Instance Private IP
+    Value: !GetAtt Ec2InstanceOfSlave02.PrivateIp
+  Slave02Ec2InstancePublicIp:
+    Description: Slave02 Instance Public IP
+    Value: !GetAtt Ec2InstanceOfSlave02.PublicIp
+    Condition: IsAssociatedPublicIp
+
+  IdOfInstanceSlave03:
+    Description: the id of Slave 03 Node Instance if created
+    Value: !Ref Ec2InstanceOfSlave03
+  Slave03Ec2InstancePrivateIp:
+    Description: Slave03 Instance Private IP
+    Value: !GetAtt Ec2InstanceOfSlave03.PrivateIp
+  Slave03Ec2InstancePublicIp:
+    Description: Slave03 Instance Public IP
+    Value: !GetAtt Ec2InstanceOfSlave03.PublicIp
+    Condition: IsAssociatedPublicIp
diff --git a/cloudformation_templates/ec2-cluster-static-services.yaml b/cloudformation_templates/ec2-cluster-static-services.yaml
new file mode 100644
index 0000000..6916c63
--- /dev/null
+++ b/cloudformation_templates/ec2-cluster-static-services.yaml
@@ -0,0 +1,211 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+AWSTemplateFormatVersion: 2010-09-09
+Description: Create Cluster for ec2 instances and deploy related services, prometheus/hivemetastore/jdk/node exporter
+Parameters:
+  # Must passed Parameters
+  SubnetId:
+    Type: String
+  SecurityGroupId:
+    Type: String
+  InstanceProfileId:
+    Type: String
+    Description: Must be created at pre-step
+  DbHost:
+    Type: String
+    Description: Db Host for RDS instance
+
+  DbUser:
+    Type: String
+    Description: Db User for RDS instance
+  DbPassword:
+    Type: String
+    Description: Db password for RDS instance
+  DbPort:
+    Type: String
+    Description: Db port for RDS instance
+
+  EMREc2KeyName:
+    Type: String
+
+#   Optional Parameters
+  Ec2Mode:
+    Type: String
+    Default: test
+    AllowedValues:
+      - test
+      - product
+  AssociatedPublicIp:
+    Type: String
+    Default: false
+    AllowedValues:
+      - false
+      - true
+  BucketFullPath:
+    Type: String
+    Description: example s3://xxx/kylin
+  BucketPath:
+    Type: String
+    Description: example /xxx/kylin which Url without prefix s3:/
+  StaticServicesScriptFileName:
+    Type: String
+    Default: prepare-ec2-env-for-static-services.sh
+  Ec2InstanceTypeForStaticServices:
+    Description: EC2 instance type
+    Type: String
+    ConstraintDescription: must be a valid EC2 instance type.
+    Default: m5.2xlarge
+    AllowedValues:
+      - m5.2xlarge
+      - m5.4xlarge
+  Ec2InstanceTypeForStaticServicesForTest:
+    Description: EC2 instance type
+    Type: String
+    ConstraintDescription: must be a valid EC2 instance type.
+    Default: m5.xlarge
+    AllowedValues:
+      - m5.xlarge
+  Ec2VolumeSizeForStaticServicesNode:
+    Type: Number
+    Default: 20
+    MinValue: 20
+    MaxValue: 30
+  Ec2VolumnTypeForStaticServicesNode:
+    Type: String
+    Default: gp2
+    AllowedValues:
+      - gp2
+      - gp3
+      - io1
+      - io2
+      - sc1
+      - st1
+      - standard
+  Ec2VolumeSizeForStaticServicesNodeForTest:
+    Type: Number
+    Default: 20
+    MinValue: 20
+    MaxValue: 20
+  Ec2VolumnTypeForStaticServicesNodeForTest:
+    Type: String
+    Default: gp2
+    AllowedValues:
+      - gp2
+
+Mappings:
+  AWSRegionArch2AMI:
+    cn-north-1:
+      HVMebs: ami-00ac27054b887ff5c
+    cn-northwest-1:
+      HVMebs: ami-0135cb179d33fbe3e
+
+Conditions:
+  NotNullSubnetId:
+    !Not [!Equals [!Ref SubnetId, ""]]
+  NotNullEc2KeyName:
+    !Not [!Equals [!Ref EMREc2KeyName, ""]]
+  IsProductMode: !Equals [!Ref Ec2Mode, "product"]
+  ValidConfigurationForEc2: !And
+    - !Condition NotNullSubnetId
+    - !Condition NotNullEc2KeyName
+  IsAssociatedPublicIp: !Equals [!Ref AssociatedPublicIp, "true"]
+
+Resources:
+  Ec2InstanceOfStaticServicesNode:
+    Type: AWS::EC2::Instance
+    DeletionPolicy: Delete
+    Condition: ValidConfigurationForEc2
+    Properties:
+      ImageId: !FindInMap
+        - AWSRegionArch2AMI
+        - !Ref 'AWS::Region'
+        - HVMebs
+      Tags:
+        - Key: Project
+          Value: Kylin4
+        - Key: Name
+          Value: StaticServices Node
+      InstanceType:
+        !If
+          - IsProductMode
+          - !Ref Ec2InstanceTypeForStaticServices
+          - !Ref Ec2InstanceTypeForStaticServicesForTest
+      IamInstanceProfile: !Ref InstanceProfileId
+      NetworkInterfaces:
+        - DeviceIndex: 0
+          Description: Auto create for Master node in benchmark
+          AssociatePublicIpAddress: !Ref AssociatedPublicIp
+          DeleteOnTermination: true
+          SubnetId: !Ref SubnetId
+          GroupSet:
+            - !Ref SecurityGroupId
+      BlockDeviceMappings:
+        - !If
+          - IsProductMode
+          - DeviceName: /dev/xvda
+            Ebs:
+              VolumeSize: !Ref Ec2VolumeSizeForStaticServicesNode
+              VolumeType: !Ref Ec2VolumnTypeForStaticServicesNode
+              DeleteOnTermination: true
+          - DeviceName: /dev/xvda
+            Ebs:
+              VolumeSize: !Ref Ec2VolumeSizeForStaticServicesNodeForTest
+              VolumeType: !Ref Ec2VolumnTypeForStaticServicesNodeForTest
+              DeleteOnTermination: true
+      KeyName: !Ref EMREc2KeyName
+      UserData:
+        Fn::Base64:
+          Fn::Sub:
+            - |
+              #!/bin/bash -xe
+              cd /home/ec2-user
+              aws s3 cp ${PrivateBucketFullPath}/scripts/${PrivateStaticServicesScriptFileName} . --region ${PrivateRegion}
+              bash ${PrivateStaticServicesScriptFileName} --bucket-url ${PrivateBucketPath} --region ${PrivateRegion} --db-host ${PrivateDbHost} --db-password ${PrivateDbPassword} --db-user ${PrivateDbUser} --db-port ${PrivateDbPort}
+              echo " Static Services are ready ..."
+            - PrivateBucketFullPath: !Ref BucketFullPath
+              PrivateStaticServicesScriptFileName: !Ref StaticServicesScriptFileName
+              PrivateBucketPath: !Ref BucketPath
+              PrivateRegion: !Ref AWS::Region
+              PrivateDbHost: !Ref DbHost
+              PrivateDbPassword: !Ref DbPassword
+              PrivateDbUser: !Ref DbUser
+              PrivateDbPort: !Ref DbPort
+
+Outputs:
+  # Env parameters
+  PublicSubnetIdDependsOnDNode:
+    Description: Public Subnet Id for distribution node
+    Value: !Ref SubnetId
+  SecurityGroupIDDependsOnDNode:
+    Description: Secrurity Group ID for distribution node
+    Value: !Ref SecurityGroupId
+  KeyPairNameDependsOnDNode:
+    Description: KeyPair for distribution node
+    Value: !Ref EMREc2KeyName
+
+  # Instance parameters
+  IdOfInstance:
+    Description: the id of StaticServices Node Instance
+    Value: !Ref Ec2InstanceOfStaticServicesNode
+  StaticServicesNodePrivateIp:
+    Description: StaticServices Private IP
+    Value: !GetAtt Ec2InstanceOfStaticServicesNode.PrivateIp
+  StaticServicesNodePublicIp:
+    Description: StaticServices Public IP
+    Value: !GetAtt Ec2InstanceOfStaticServicesNode.PublicIp
+    Condition: IsAssociatedPublicIp
diff --git a/cloudformation_templates/ec2-cluster-zk.yaml b/cloudformation_templates/ec2-cluster-zk.yaml
new file mode 100644
index 0000000..7d43e8d
--- /dev/null
+++ b/cloudformation_templates/ec2-cluster-zk.yaml
@@ -0,0 +1,322 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+AWSTemplateFormatVersion: 2010-09-09
+Description: Create Cluster for ec2 instances and deploy related services, hadoop/spark/zookeeper/hive/jdk/kylin4
+Parameters:
+  # Must passed Parameters
+  SubnetId:
+    Type: String
+  SecurityGroupId:
+    Type: String
+  EMREc2KeyName:
+    Type: String
+  InstanceProfileId:
+    Type: String
+    Description: Must be created at pre-step
+
+#   Optional Parameters
+  Ec2Mode:
+    Type: String
+    Default: test
+    AllowedValues:
+      - test
+      - product
+  AssociatedPublicIp:
+    Type: String
+    Default: false
+    AllowedValues:
+      - false
+      - true
+  BucketFullPath:
+    Type: String
+    Description: example s3://xxx/kylin
+  BucketPath:
+    Type: String
+    Description: example /xxx/kylin which Url without prefix s3:/
+  ZookeeperScriptFileName:
+    Type: String
+    Default: prepare-ec2-env-for-zk.sh
+  Ec2InstanceTypeForZookeeper:
+    Description: EC2 instance type
+    Type: String
+    ConstraintDescription: must be a valid EC2 instance type.
+    Default: m5.xlarge
+    AllowedValues:
+      - m5.xlarge
+      - m5.2xlarge
+  Ec2InstanceTypeForZookeeperForTest:
+    Description: EC2 instance type
+    Type: String
+    ConstraintDescription: must be a valid EC2 instance type.
+    Default: m5.xlarge
+    AllowedValues:
+      - m5.xlarge
+  Ec2VolumeSizeForZookeeperNode:
+    Type: Number
+    Default: 10
+    MinValue: 10
+    MaxValue: 30
+  Ec2VolumnTypeForZookeeperNode:
+    Type: String
+    Default: gp2
+    AllowedValues:
+      - gp2
+      - gp3
+      - io1
+      - io2
+      - sc1
+      - st1
+      - standard
+  Ec2VolumeSizeForZookeeperNodeForTest:
+    Type: Number
+    Default: 10
+    MinValue: 10
+    MaxValue: 10
+  Ec2VolumnTypeForZookeeperNodeForTest:
+    Type: String
+    Default: gp2
+    AllowedValues:
+      - gp2
+
+Mappings:
+  AWSRegionArch2AMI:
+    cn-north-1:
+      HVMebs: ami-00ac27054b887ff5c
+    cn-northwest-1:
+      HVMebs: ami-0135cb179d33fbe3e
+
+Conditions:
+  NotNullSubnetId:
+    !Not [!Equals [!Ref SubnetId, ""]]
+  NotNullEc2KeyName:
+    !Not [!Equals [!Ref EMREc2KeyName, ""]]
+  IsProductMode: !Equals [!Ref Ec2Mode, "product"]
+  ValidConfigurationForEc2: !And
+    - !Condition NotNullSubnetId
+    - !Condition NotNullEc2KeyName
+  IsAssociatedPublicIp: !Equals [!Ref AssociatedPublicIp, "true"]
+
+Resources:
+  Ec2InstanceOfZookeeperNode01:
+    Type: AWS::EC2::Instance
+    DeletionPolicy: Delete
+    Condition: ValidConfigurationForEc2
+    Properties:
+      ImageId: !FindInMap
+        - AWSRegionArch2AMI
+        - !Ref 'AWS::Region'
+        - HVMebs
+      Tags:
+        - Key: Project
+          Value: Kylin4
+        - Key: Name
+          Value: Zookeeper Node 1
+      InstanceType:
+        !If
+          - IsProductMode
+          - !Ref Ec2InstanceTypeForZookeeper
+          - !Ref Ec2InstanceTypeForZookeeperForTest
+      IamInstanceProfile: !Ref InstanceProfileId
+      NetworkInterfaces:
+        - DeviceIndex: 0
+          Description: Auto create for Master node in benchmark
+          AssociatePublicIpAddress: !Ref AssociatedPublicIp
+          DeleteOnTermination: true
+          SubnetId: !Ref SubnetId
+          GroupSet:
+            - !Ref SecurityGroupId
+      BlockDeviceMappings:
+        - !If
+          - IsProductMode
+          - DeviceName: /dev/xvda
+            Ebs:
+              VolumeSize: !Ref Ec2VolumeSizeForZookeeperNode
+              VolumeType: !Ref Ec2VolumnTypeForZookeeperNode
+              DeleteOnTermination: true
+          - DeviceName: /dev/xvda
+            Ebs:
+              VolumeSize: !Ref Ec2VolumeSizeForZookeeperNodeForTest
+              VolumeType: !Ref Ec2VolumnTypeForZookeeperNodeForTest
+              DeleteOnTermination: true
+      KeyName: !Ref EMREc2KeyName
+      UserData:
+        Fn::Base64:
+          Fn::Sub:
+            - |
+              #!/bin/bash -xe
+              cd /home/ec2-user
+              aws s3 cp ${PrivateBucketFullPath}/scripts/${PrivateZookeeperScriptFileName} . --region ${PrivateRegion}
+              bash ${PrivateZookeeperScriptFileName} --bucket-url ${PrivateBucketPath} --region ${PrivateRegion} --zk-num ${privateZkNum}
+              echo "Zookeeper is ready ..."
+            - PrivateBucketFullPath: !Ref BucketFullPath
+              PrivateZookeeperScriptFileName: !Ref ZookeeperScriptFileName
+              PrivateBucketPath: !Ref BucketPath
+              PrivateRegion: !Ref AWS::Region
+              privateZkNum: 1
+
+  Ec2InstanceOfZookeeperNode02:
+    Type: AWS::EC2::Instance
+    DeletionPolicy: Delete
+    Condition: ValidConfigurationForEc2
+    Properties:
+      ImageId: !FindInMap
+        - AWSRegionArch2AMI
+        - !Ref 'AWS::Region'
+        - HVMebs
+      Tags:
+        - Key: Project
+          Value: Kylin4
+        - Key: Name
+          Value: Zookeeper Node 2
+      InstanceType:
+        !If
+        - IsProductMode
+        - !Ref Ec2InstanceTypeForZookeeper
+        - !Ref Ec2InstanceTypeForZookeeperForTest
+      IamInstanceProfile: !Ref InstanceProfileId
+      NetworkInterfaces:
+        - DeviceIndex: 0
+          Description: Auto create for Master node in benchmark
+          AssociatePublicIpAddress: !Ref AssociatedPublicIp
+          DeleteOnTermination: true
+          SubnetId: !Ref SubnetId
+          GroupSet:
+            - !Ref SecurityGroupId
+      BlockDeviceMappings:
+        - !If
+          - IsProductMode
+          - DeviceName: /dev/xvda
+            Ebs:
+              VolumeSize: !Ref Ec2VolumeSizeForZookeeperNode
+              VolumeType: !Ref Ec2VolumnTypeForZookeeperNode
+              DeleteOnTermination: true
+          - DeviceName: /dev/xvda
+            Ebs:
+              VolumeSize: !Ref Ec2VolumeSizeForZookeeperNodeForTest
+              VolumeType: !Ref Ec2VolumnTypeForZookeeperNodeForTest
+              DeleteOnTermination: true
+      KeyName: !Ref EMREc2KeyName
+      UserData:
+        Fn::Base64:
+          Fn::Sub:
+            - |
+              #!/bin/bash -xe
+              cd /home/ec2-user
+              aws s3 cp ${PrivateBucketFullPath}/scripts/${PrivateZookeeperScriptFileName} . --region ${PrivateRegion}
+              bash ${PrivateZookeeperScriptFileName} --bucket-url ${PrivateBucketPath} --region ${PrivateRegion} --zk-num ${privateZkNum}
+              echo "Zookeeper is ready ..."
+            - PrivateBucketFullPath: !Ref BucketFullPath
+              PrivateZookeeperScriptFileName: !Ref ZookeeperScriptFileName
+              PrivateBucketPath: !Ref BucketPath
+              PrivateRegion: !Ref AWS::Region
+              privateZkNum: 2
+
+  Ec2InstanceOfZookeeperNode03:
+    Type: AWS::EC2::Instance
+    DeletionPolicy: Delete
+    Condition: ValidConfigurationForEc2
+    Properties:
+      ImageId: !FindInMap
+        - AWSRegionArch2AMI
+        - !Ref 'AWS::Region'
+        - HVMebs
+      Tags:
+        - Key: Project
+          Value: Kylin4
+        - Key: Name
+          Value: Zookeeper Node 3
+      InstanceType:
+        !If
+        - IsProductMode
+        - !Ref Ec2InstanceTypeForZookeeper
+        - !Ref Ec2InstanceTypeForZookeeperForTest
+      IamInstanceProfile: !Ref InstanceProfileId
+      NetworkInterfaces:
+        - DeviceIndex: 0
+          Description: Auto create for Master node in benchmark
+          AssociatePublicIpAddress: !Ref AssociatedPublicIp
+          DeleteOnTermination: true
+          SubnetId: !Ref SubnetId
+          GroupSet:
+            - !Ref SecurityGroupId
+      BlockDeviceMappings:
+        - !If
+          - IsProductMode
+          - DeviceName: /dev/xvda
+            Ebs:
+              VolumeSize: !Ref Ec2VolumeSizeForZookeeperNode
+              VolumeType: !Ref Ec2VolumnTypeForZookeeperNode
+              DeleteOnTermination: true
+          - DeviceName: /dev/xvda
+            Ebs:
+              VolumeSize: !Ref Ec2VolumeSizeForZookeeperNodeForTest
+              VolumeType: !Ref Ec2VolumnTypeForZookeeperNodeForTest
+              DeleteOnTermination: true
+      KeyName: !Ref EMREc2KeyName
+      UserData:
+        Fn::Base64:
+          Fn::Sub:
+            - |
+              #!/bin/bash -xe
+              cd /home/ec2-user
+              aws s3 cp ${PrivateBucketFullPath}/scripts/${PrivateZookeeperScriptFileName} . --region ${PrivateRegion}
+              bash ${PrivateZookeeperScriptFileName} --bucket-url ${PrivateBucketPath} --region ${PrivateRegion} --zk-num ${privateZkNum}
+              echo "Zookeeper is ready ..."
+            - PrivateBucketFullPath: !Ref BucketFullPath
+              PrivateZookeeperScriptFileName: !Ref ZookeeperScriptFileName
+              PrivateBucketPath: !Ref BucketPath
+              PrivateRegion: !Ref AWS::Region
+              privateZkNum: 3
+
+Outputs:
+  # Instance 01 parameters
+  IdOfInstanceZookeeper01:
+    Description: the id of Zookeeper Node Instance
+    Value: !Ref Ec2InstanceOfZookeeperNode01
+  ZookeeperNode01PrivateIp:
+    Description: Zookeeper Private IP
+    Value: !GetAtt Ec2InstanceOfZookeeperNode01.PrivateIp
+  ZookeeperNode01PublicIp:
+    Description: Zookeeper Public IP
+    Value: !GetAtt Ec2InstanceOfZookeeperNode01.PublicIp
+    Condition: IsAssociatedPublicIp
+
+  # Instance 02 parameters
+  IdOfInstanceZookeeper02:
+    Description: the id of Zookeeper Node Instance
+    Value: !Ref Ec2InstanceOfZookeeperNode02
+  ZookeeperNode02PrivateIp:
+    Description: Zookeeper Private IP
+    Value: !GetAtt Ec2InstanceOfZookeeperNode02.PrivateIp
+  ZookeeperNode02PublicIp:
+    Description: Zookeeper Public IP
+    Value: !GetAtt Ec2InstanceOfZookeeperNode02.PublicIp
+    Condition: IsAssociatedPublicIp
+
+
+  # Instance 03 parameters
+  IdOfInstanceZookeeper03:
+    Description: the id of Zookeeper Node Instance
+    Value: !Ref Ec2InstanceOfZookeeperNode03
+  ZookeeperNode03PrivateIp:
+    Description: Zookeeper Private IP
+    Value: !GetAtt Ec2InstanceOfZookeeperNode03.PrivateIp
+  ZookeeperNode03PublicIp:
+    Description: Zookeeper Public IP
+    Value: !GetAtt Ec2InstanceOfZookeeperNode03.PublicIp
+    Condition: IsAssociatedPublicIp
diff --git a/cloudformation_templates/ec2-or-emr-vpc.yaml b/cloudformation_templates/ec2-or-emr-vpc.yaml
new file mode 100644
index 0000000..e5c5236
--- /dev/null
+++ b/cloudformation_templates/ec2-or-emr-vpc.yaml
@@ -0,0 +1,283 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+AWSTemplateFormatVersion: "2010-09-09"
+Description: create vpc, subnet and security for emr or ec2 's benchmark
+Parameters:
+  # Must passed parameters
+  # Optional Parameters
+  KylinVersion:
+    Type: String
+    Description: this version is for tag
+    Default: 4.0.0
+    AllowedValues:
+      - 4.0.0
+  ClusterType:
+    Type: String
+    Default: ec2
+    Description: this type is for tag
+    AllowedValues:
+      - ec2
+  VpcCidrBlock:
+    Type: String
+    Default: 10.1.0.0/16
+  SubnetCidrBlock:
+    Type: String
+    Default: 10.1.0.0/24
+  Subnet02CidrBlock:
+    Type: String
+    Default: 10.1.1.0/24
+  Ec2OperationRole:
+    Type: String
+    Description: Should be created by pre-step, must not be null !
+  CidrIp:
+    Type: String
+    Description: An inbound rule permits instances to receive traffic from the specified IPv4 or IPv6 CIDR address range, or from the instances associated with the specified security group.
+
+Conditions:
+  NotNullCidrIp:
+    !Not [!Equals [!Ref CidrIp, ""]]
+
+Mappings:
+  Region2Az:
+    cn-northwest-1:
+      '1': cn-northwest-1a
+      '2': cn-northwest-1b
+
+Resources:
+  Ec2InstanceProfile:
+    Type: AWS::IAM::InstanceProfile
+    DeletionPolicy: Delete
+    Properties:
+      Roles:
+        - !Ref Ec2OperationRole
+  Ec2OrEmrBenchMarkVpc:
+    Type: AWS::EC2::VPC
+    Condition: NotNullCidrIp
+    Properties:
+      CidrBlock: !Ref VpcCidrBlock
+      EnableDnsSupport: true
+      EnableDnsHostnames: true
+      Tags:
+        - Key: Name
+          Value: !Join
+            - ''
+            - - Kylin
+              - !Ref KylinVersion
+              - EmrOrEc2
+              - Vpc
+    DeletionPolicy: Delete
+  Ec2OrEmrBenchMarkSubnet:
+    Type: AWS::EC2::Subnet
+    DeletionPolicy: Delete
+    Properties:
+      VpcId: !Ref Ec2OrEmrBenchMarkVpc
+      CidrBlock: !Ref SubnetCidrBlock
+      AvailabilityZone: !FindInMap
+        - Region2Az
+        - !Ref 'AWS::Region'
+        - '1'
+      Tags:
+        - Key: Name
+          Value: !Join
+            - ''
+            - - Kylin
+              - !Ref KylinVersion
+              - !Ref ClusterType
+              - pub1
+  Ec2OrEmrBenchMarkSubnet02:
+    Type: AWS::EC2::Subnet
+    DeletionPolicy: Delete
+    Properties:
+      VpcId: !Ref Ec2OrEmrBenchMarkVpc
+      CidrBlock: !Ref Subnet02CidrBlock
+      AvailabilityZone: !FindInMap
+        - Region2Az
+        - !Ref 'AWS::Region'
+        - '2'
+      Tags:
+        - Key: Name
+          Value: !Join
+            - ''
+            - - Kylin
+              - !Ref KylinVersion
+              - !Ref ClusterType
+              - pub2
+  Ec2OrEmrBenchMarkDBSubnetGroup:
+    Type: AWS::RDS::DBSubnetGroup
+    DeletionPolicy: Delete
+    Properties:
+      Tags:
+        - Key: Project
+          Value: kylin4
+        - Key: Name
+          Value: subnet group for db instance
+      DBSubnetGroupDescription: subnet for db instance
+      SubnetIds:
+        - !Ref Ec2OrEmrBenchMarkSubnet
+        - !Ref Ec2OrEmrBenchMarkSubnet02
+
+  Ec2OrEmrBenchMarkInternetGateway:
+    DeletionPolicy: Delete
+    Type: AWS::EC2::InternetGateway
+    Properties:
+      Tags:
+        - Key: Name
+          Value: !Join
+            - ''
+            - - Kylin
+              - !Ref KylinVersion
+              - !Ref ClusterType
+              - InternetGateWay
+  AttachGateway:
+    Type: AWS::EC2::VPCGatewayAttachment
+    DeletionPolicy: Delete
+    Properties:
+      VpcId: !Ref Ec2OrEmrBenchMarkVpc
+      InternetGatewayId: !Ref Ec2OrEmrBenchMarkInternetGateway
+  Ec2OrEmrBenchMarkRoutetable:
+    Type: AWS::EC2::RouteTable
+    DeletionPolicy: Delete
+    Properties:
+      VpcId: !Ref Ec2OrEmrBenchMarkVpc
+      Tags:
+        - Key: Name
+          Value: !Join
+            - ''
+            - - Kylin
+              - !Ref KylinVersion
+              - !Ref ClusterType
+              - Routetable
+  Ec2OrEmrBenchMarkRoute:
+    Type: AWS::EC2::Route
+    DependsOn:
+      - Ec2OrEmrBenchMarkInternetGateway
+      - Ec2OrEmrBenchMarkRoutetable
+    DeletionPolicy: Delete
+    Properties:
+      RouteTableId: !Ref Ec2OrEmrBenchMarkRoutetable
+      DestinationCidrBlock: 0.0.0.0/0
+      GatewayId: !Ref Ec2OrEmrBenchMarkInternetGateway
+  SubnetRouteTableAssociation:
+    Type: 'AWS::EC2::SubnetRouteTableAssociation'
+    DeletionPolicy: Delete
+    Properties:
+      SubnetId: !Ref Ec2OrEmrBenchMarkSubnet
+      RouteTableId: !Ref Ec2OrEmrBenchMarkRoutetable
+  Subnet2RouteTableAssociation:
+    Type: 'AWS::EC2::SubnetRouteTableAssociation'
+    DeletionPolicy: Delete
+    Properties:
+      SubnetId: !Ref Ec2OrEmrBenchMarkSubnet02
+      RouteTableId: !Ref Ec2OrEmrBenchMarkRoutetable
+  Ec2OrEmrBenchMarkSecurityGroup:
+    Type: AWS::EC2::SecurityGroup
+    DependsOn: Ec2OrEmrBenchMarkVpc
+    Properties:
+      VpcId: !Ref Ec2OrEmrBenchMarkVpc
+      GroupDescription: Open up SSH access and specify range of ports to itself
+      Tags:
+        - Key: Name
+          Value: !Join
+            - ''
+            - - Kylin
+              - !Ref KylinVersion
+              - !Ref ClusterType
+              - SecurityGroup
+      SecurityGroupIngress:
+        # For SSH
+        - IpProtocol: tcp
+          FromPort: '22'
+          ToPort: '22'
+          CidrIp: !Ref CidrIp
+        # For Grafana
+        - IpProtocol: tcp
+          FromPort: '3000'
+          ToPort: '3000'
+          CidrIp: !Ref CidrIp
+        # For Prometheus
+        - IpProtocol: tcp
+          FromPort: '9090'
+          ToPort: '9100'
+          CidrIp: !Ref CidrIp
+        # For Kylin
+        - IpProtocol: tcp
+          FromPort: '7070'
+          ToPort: '7070'
+          CidrIp: !Ref CidrIp
+        # For Spark Master
+        - IpProtocol: tcp
+          FromPort: '8080'
+          ToPort: '8080'
+          CidrIp: !Ref CidrIp
+        # For Spark Worker/Executor
+        - IpProtocol: tcp
+          FromPort: '4040'
+          ToPort: '4050'
+          CidrIp: !Ref CidrIp
+    DeletionPolicy: Delete
+  Ec2OrEmrBenchMarkSecurityGroupFullTcpIngress:
+    Type: AWS::EC2::SecurityGroupIngress
+    DeletionPolicy: Delete
+    Properties:
+      IpProtocol: tcp
+      SourceSecurityGroupId: !Ref Ec2OrEmrBenchMarkSecurityGroup
+      FromPort: 0
+      ToPort: 65535
+      GroupId: !Ref Ec2OrEmrBenchMarkSecurityGroup
+  Ec2OrEmrBenchMarkSecurityGroupFullUdpIngress:
+    Type: AWS::EC2::SecurityGroupIngress
+    DeletionPolicy: Delete
+    Properties:
+      IpProtocol: udp
+      SourceSecurityGroupId: !Ref Ec2OrEmrBenchMarkSecurityGroup
+      FromPort: 0
+      ToPort: 65535
+      GroupId: !Ref Ec2OrEmrBenchMarkSecurityGroup
+  Ec2OrEmrBenchMarkSecurityGroupFullIcmpIpv4Ingress:
+    Type: AWS::EC2::SecurityGroupIngress
+    DeletionPolicy: Delete
+    Description: special port range from -1 to -1 that means full
+    Properties:
+      IpProtocol: icmp
+      SourceSecurityGroupId: !Ref Ec2OrEmrBenchMarkSecurityGroup
+      FromPort: -1
+      ToPort: -1
+      GroupId: !Ref Ec2OrEmrBenchMarkSecurityGroup
+
+Outputs:
+  VpcId:
+    Description: the id of VPC
+    Value: !Ref Ec2OrEmrBenchMarkVpc
+  SubnetId:
+    Description: the id of subnet
+    Value: !Ref Ec2OrEmrBenchMarkSubnet
+  CidrIpOfVPC:
+    Description: the cidr block ip of VPC
+    Value: !Ref VpcCidrBlock
+  CidrIpOfSubnet:
+    Description: the cidr block ip of subnet
+    Value: !Ref SubnetCidrBlock
+  SecurityGroupId:
+    Description: the id of security group
+    Value: !Ref Ec2OrEmrBenchMarkSecurityGroup
+  InstanceProfileId:
+    Description: Instance profile for cluster
+    Value: !Ref Ec2InstanceProfile
+  SubnetGroupName:
+    Description: Subnet group name for DB instance
+    Value: !Ref Ec2OrEmrBenchMarkDBSubnetGroup
diff --git a/clouds/__init__.py b/clouds/__init__.py
new file mode 100644
index 0000000..cce3aca
--- /dev/null
+++ b/clouds/__init__.py
@@ -0,0 +1,16 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
diff --git a/clouds/aws.py b/clouds/aws.py
new file mode 100644
index 0000000..d661851
--- /dev/null
+++ b/clouds/aws.py
@@ -0,0 +1,385 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import logging
+import os
+from ast import literal_eval
+from concurrent.futures import ThreadPoolExecutor
+from itertools import repeat
+from typing import Optional, Dict, List
+
+from constant.config import Config
+from constant.deployment import ScaleType, NodeType
+from constant.kylin_properties_params import KylinProperties
+from constant.path import KYLIN_PROPERTIES_TEMPLATE_DIR
+from constant.yaml_params import Params
+from instances.aws_instance import AWSInstance
+from utils import Utils
+
+logger = logging.getLogger(__name__)
+
+
+class AWS:
+
+    def __init__(self, config) -> None:
+        self.cloud_instance = AWSInstance(config)
+        self.config = config
+
+    @property
+    def is_cluster_ready(self) -> bool:
+        if self.is_instances_ready:
+            return True
+        msg = f'Current cluster is not ready.'
+        logger.warning(msg)
+        return False
+
+    @property
+    def is_prepared_for_scale_cluster(self) -> bool:
+        if self.is_prepared_for_cluster:
+            return True
+        msg = f'Current env for deploying a cluster is not ready.'
+        logger.warning(msg)
+        return False
+
+    @property
+    def is_instances_ready(self) -> bool:
+        return self.cloud_instance.is_ec2_stacks_ready()
+
+    @property
+    def is_prepared_for_cluster(self) -> bool:
+        return self.cloud_instance.is_prepared_for_scale_cluster()
+
+    @property
+    def is_cluster_terminated(self) -> bool:
+        if self.is_instances_terminated:
+            return True
+        msg = 'Current cluster is alive, please destroy cluster first.'
+        logger.warning(msg)
+        return False
+
+    @property
+    def is_instances_terminated(self) -> bool:
+        return self.cloud_instance.is_ec2_stacks_terminated()
+
+    @property
+    def kylin_stack_name(self) -> str:
+        return self.cloud_instance.kylin_stack_name
+
+    @property
+    def is_associated_public_ip(self) -> bool:
+        return self.config[Params.ASSOSICATED_PUBLIC_IP.value] == 'true'
+
+    @property
+    def is_destroy_all(self) -> bool:
+        return self.config[Params.ALWAYS_DESTROY_ALL.value] is True
+
+    def is_target_cluster_ready(self, cluster_num: int) -> bool:
+        if self.is_target_cluster_instances_ready(cluster_num):
+            return True
+        msg = f'Current cluster {cluster_num} is not ready, please deploy the cluster first.'
+        logger.warning(msg)
+        return False
+    
+    def is_target_cluster_instances_ready(self, cluster_num: int) -> bool:
+        return self.cloud_instance.is_target_ec2_stacks_ready(cluster_num)
+
+    def validate_params(self) -> None:
+        self.cloud_instance.valid_s3_bucket()
+        self.cloud_instance.valid_iam_role()
+        self.cloud_instance.valid_key_pair()
+        self.cloud_instance.valid_cidr_ip()
+
+    def get_resources(self, stack_name: str) -> Dict:
+        return self.cloud_instance.get_stack_output(stack_name)
+
+    def prepare_for_whole_cluster(self) -> None:
+        self.validate_params()
+        self.cloud_instance.create_vpc_stack()
+        self.cloud_instance.create_rds_stack()
+        self.cloud_instance.create_static_service_stack()
+
+    def init_cluster(self) -> None:
+        if not self.is_instances_ready:
+            self.cloud_instance.create_zk_stack()
+            # Need to refresh its config and start them after created zks
+            self.cloud_instance.after_create_zk_cluster()
+            self.cloud_instance.create_spark_master_stack()
+            self.cloud_instance.create_spark_slave_stack()
+            # Before to create a Kylin node, init local kylin.properties and upload it.
+            self.init_kylin_properties()
+            self.upload_kylin_properties()
+            # # Then create a kylin node
+            self.cloud_instance.create_kylin_stack()
+        self.cloud_instance.start_prometheus_first_time()
+        logger.info('Cluster start successfully.')
+
+    def get_kylin_address(self):
+        kylin_resources = self.get_kylin_resources()
+        kylin4_address = kylin_resources.get(Params.KYLIN4_PRIVATE_IP.value)
+        if self.is_associated_public_ip:
+            kylin4_address = kylin_resources.get(Params.KYLIN4_PUBLIC_IP.value)
+        return kylin4_address
+
+    def get_kylin_resources(self):
+        if not self.is_cluster_ready:
+            self.init_cluster()
+        kylin_resources = self.get_resources(self.kylin_stack_name)
+        return kylin_resources
+
+    def destroy_default_cluster(self):
+        self.terminate_ec2_cluster()
+
+    def terminate_ec2_cluster(self) -> Optional[Dict]:
+        if self.is_cluster_terminated:
+            return
+        self.cloud_instance.terminate_spark_slave_stack()
+        self.cloud_instance.terminate_spark_master_stack()
+        self.cloud_instance.terminate_kylin_stack()
+        self.cloud_instance.terminate_zk_stack()
+        logger.info('Cluster terminated useless nodes.')
+
+    def destroy_rds_and_vpc(self) -> None:
+        if not self.is_destroy_all:
+            return
+        logger.info('Prepare to destroy RDS and VPC and monitor node.')
+        # destroy all will delete rds, please be careful.
+        self.cloud_instance.terminate_static_service_stack()
+        self.cloud_instance.terminate_rds_stack()
+        self.cloud_instance.terminate_vpc_stack()
+        logger.info('Destroy RDS and VPC and monitor node done.')
+
+    def alive_nodes(self):
+        cluster_nums = self.generate_scaled_cluster_nums()
+        self.cloud_instance.alive_nodes(cluster_nums=cluster_nums)
+
+    def scale_up(self, node_type: str, cluster_num: int = None, is_destroy: bool = False) -> None:
+        # validate scale_type & node_type
+        self.validate_node_type(node_type)
+        node_nums = self.generate_scaled_up_list(node_type)
+
+        if node_type == NodeType.KYLIN.value:
+            if self.is_kylin_properties_exists(cluster_num=cluster_num):
+                self.init_kylin_properties(cluster_num=cluster_num)
+                self.upload_kylin_properties(cluster_num=cluster_num)
+
+        exec_pool = ThreadPoolExecutor(max_workers=10)
+        with exec_pool as pool:
+            if node_type == NodeType.KYLIN.value:
+                pool.map(self.cloud_instance.scale_up_kylin, node_nums, repeat(cluster_num, len(node_nums)))
+            else:
+                pool.map(self.cloud_instance.scale_up_worker, node_nums, repeat(cluster_num, len(node_nums)))
+
+    def scale_down(self, node_type: str, cluster_num: int = None, is_destroy: bool = False) -> None:
+        self.validate_node_type(node_type)
+        node_nums = self.generate_scaled_down_list(node_type, is_destroy=is_destroy)
+
+        exec_pool = ThreadPoolExecutor(max_workers=10)
+        with exec_pool as pool:
+            if node_type == NodeType.KYLIN.value:
+                pool.map(self.cloud_instance.scale_down_kylin, node_nums, repeat(cluster_num, len(node_nums)))
+            else:
+                pool.map(self.cloud_instance.scale_down_worker, node_nums, repeat(cluster_num, len(node_nums)))
+
+    def launch_clusters(self, cluster_nums: List[int] = None) -> None:
+        if not cluster_nums:
+            cluster_nums = self.generate_scaled_cluster_nums()
+
+        for num in cluster_nums:
+            self.launch_cluster(cluster_num=num)
+
+    def launch_cluster(self, cluster_num: int) -> None:
+        # Before scale up cluster, check the related kylin.properties must exists.
+        self.validate_cluster(cluster_num=cluster_num)
+        self.is_kylin_properties_exists(cluster_num=cluster_num)
+        self.cloud_instance.scale_up_basic_services_for_cluster(cluster_num=cluster_num)
+        self.init_kylin_properties(cluster_num=cluster_num)
+        self.upload_kylin_properties(cluster_num=cluster_num)
+        self.cloud_instance.scale_up_kylin_for_cluster(cluster_num=cluster_num)
+
+    def validate_cluster(self, cluster_num: int) -> None:
+        cluster_nums = self.generate_scaled_cluster_nums()
+        if cluster_num not in cluster_nums:
+            msg = f'Cluster index: {cluster_num} not in the ' \
+                  f'range of {self.config[Config.CLUSTER_INDEXES.value]}, please check.'
+            raise Exception(msg)
+
+    def destroy_clusters(self, cluster_nums: List) -> None:
+        if not cluster_nums:
+            cluster_nums = self.generate_scaled_cluster_nums()
+
+        for num in cluster_nums:
+            self.cloud_instance.destroy_cluster(num)
+
+    def is_kylin_properties_exists_in_clusters(self, cluster_nums: List) -> None:
+        for num in cluster_nums:
+            self.is_kylin_properties_exists(cluster_num=num)
+
+    @staticmethod
+    def is_kylin_properties_exists(cluster_num: int = None, properties_file: str = 'kylin.properties') -> None:
+        dest_path = os.path.join(KYLIN_PROPERTIES_TEMPLATE_DIR.format(
+            cluster_num=cluster_num if cluster_num else 'default'), properties_file)
+        if not os.path.exists(dest_path):
+            raise Exception(f'Expected {dest_path} not exists, please check.')
+
+    def restart_prometheus_server(self) -> None:
+        self.cloud_instance.restart_prometheus_server()
+
+    def after_scale_up(self, node_type: str, cluster_num: int = None) -> None:
+        logger.info(f"Checking exists prometheus config after scale up.")
+        not_exists_nodes = self.cloud_instance.check_prometheus_config_after_scale(node_type, cluster_num)
+        logger.info("Refresh prometheus config after scale up.")
+        if not_exists_nodes:
+            self.cloud_instance.refresh_prometheus_config_after_scale_up(not_exists_nodes)
+            if node_type == NodeType.KYLIN.value:
+                self.cloud_instance.refresh_prometheus_spark_driver_of_kylin_after_scale_up(not_exists_nodes)
+
+    def after_scale_down(self, node_type: str, cluster_num: int = None) -> None:
+        logger.info(f"Checking exists prometheus config after scale down.")
+        exists_nodes = self.cloud_instance.check_prometheus_config_after_scale_down(node_type, cluster_num)
+        logger.info("Refresh prometheus config after scale down.")
+        if exists_nodes:
+            self.cloud_instance.refresh_prometheus_config_after_scale_down(exists_nodes)
+            if node_type == NodeType.KYLIN.value:
+                self.cloud_instance.refresh_prometheus_spark_driver_of_kylin_after_scale_down(exists_nodes)
+
+    def after_launch_clusters(self, cluster_nums: List[int] = None) -> None:
+        # cluster_nums is none means deploy all cluster
+        if not cluster_nums:
+            cluster_nums = self.generate_scaled_cluster_nums()
+
+        logger.info(f"Checking exists prometheus config after launch clusters.")
+        not_exists_nodes = self.cloud_instance.check_prometheus_config_after_launch_clusters(cluster_nums)
+        not_exists_kylin_nodes, not_exists_spark_nodes = self.cloud_instance\
+            .check_spark_metric_config_after_launch_clusters(cluster_nums)
+        logger.info("Refresh prometheus config after launch clusters.")
+        if not_exists_nodes:
+            self.cloud_instance.refresh_prometheus_config_after_launch_clusters(not_exists_nodes)
+        if not_exists_kylin_nodes:
+            self.cloud_instance.refresh_prometheus_spark_metrics_of_kylin_in_cluster_after_scale_up(
+                not_exists_kylin_nodes)
+        if not_exists_spark_nodes:
+            self.cloud_instance.refresh_prometheus_spark_master_in_cluster_after_scale(not_exists_spark_nodes)
+
+    def after_destroy_clusters(self, cluster_nums: List[int] = None) -> None:
+        if not cluster_nums:
+            cluster_nums = self.generate_scaled_cluster_nums()
+        logger.info(f"Checking exists prometheus config after destroy cluster.")
+        exists_nodes = self.cloud_instance.check_prometheus_config_after_destroy_clusters(cluster_nums)
+        exists_kylin_nodes, exists_spark_nodes = self.cloud_instance \
+            .check_spark_metric_config_after_destroy_clusters(cluster_nums)
+        logger.info("Refresh prometheus config after destroy cluster.")
+        if exists_nodes:
+            self.cloud_instance.refresh_prometheus_config_after_destroy_clusters(exists_nodes)
+        if exists_kylin_nodes:
+            self.cloud_instance.refresh_prometheus_config_after_scale_down(exists_kylin_nodes)
+        if exists_spark_nodes:
+            self.cloud_instance.refresh_prometheus_config_after_scale_down(exists_spark_nodes)
+
+    @staticmethod
+    def validate_scale_type(scale_type: str) -> None:
+        if scale_type is None or scale_type not in [e.value for e in ScaleType]:
+            msg = f'Not supported scale type: {scale_type}.'
+            logger.error(msg)
+            raise Exception(msg)
+
+    def validate_scale_range(self) -> None:
+        if not self.cloud_instance.is_valid_scaled_range_of_kylin_nodes:
+            msg = 'Scale down range must be less than scale up range in KYLIN_SCALE_*_NODES.'
+            raise Exception(msg)
+
+        if not self.cloud_instance.is_valid_scaled_range_of_spark_worker_nodes:
+            msg = 'Scale down range must be less than scale up range in SPARK_WORKER_SCALE_*_NODES.'
+            raise Exception(msg)
+
+    @staticmethod
+    def validate_node_type(node_type: str) -> None:
+        if node_type is None or node_type not in [e.value for e in NodeType]:
+            msg = f'Not supported None node type.'
+            logger.error(msg)
+            raise Exception(msg)
+
+    def generate_scaled_up_list(self, node_type: str) -> List:
+        if node_type == NodeType.KYLIN.value:
+            kylin_nodes = literal_eval(self.config[Config.KYLIN_SCALE_UP_NODES.value])
+            return Utils.generate_nodes(kylin_nodes)
+        else:
+            worker_nodes = literal_eval(self.config[Config.SPARK_WORKER_SCALE_UP_NODES.value])
+            return Utils.generate_nodes(worker_nodes)
+
+    def generate_scaled_down_list(self, node_type: str, is_destroy: bool = False) -> List:
+        if node_type == NodeType.KYLIN.value:
+            if is_destroy:
+                scaled_range = self.config[Config.KYLIN_SCALE_UP_NODES.value]
+            else:
+                scaled_range = self.config[Config.KYLIN_SCALE_DOWN_NODES.value]
+            kylin_nodes = literal_eval(scaled_range)
+            return Utils.generate_nodes(kylin_nodes)
+        else:
+            if is_destroy:
+                scaled_range = self.config[Config.SPARK_WORKER_SCALE_UP_NODES.value]
+            else:
+                scaled_range = self.config[Config.SPARK_WORKER_SCALE_DOWN_NODES.value]
+            worker_nodes = literal_eval(scaled_range)
+            return Utils.generate_nodes(worker_nodes)
+
+    def generate_scaled_cluster_nums(self) -> List:
+        cluster_nums = literal_eval(self.config[Config.CLUSTER_INDEXES.value])
+        return Utils.generate_nodes(cluster_nums)
+
+    def validate_s3_bucket(self) -> None:
+        self.cloud_instance.valid_s3_bucket()
+
+    def init_kylin_properties_of_clusters(self, cluster_nums: List) -> None:
+        for num in cluster_nums:
+            self.init_kylin_properties(num)
+
+    def init_kylin_properties(self, cluster_num: int = None) -> None:
+        params: Dict = {
+            # DB params
+            KylinProperties.DB_HOST.value: self.cloud_instance.get_db_host(),
+            KylinProperties.DB_PORT.value: self.cloud_instance.db_port,
+            KylinProperties.DB_USER.value: self.cloud_instance.db_user,
+            KylinProperties.DB_PASSWORD.value: self.cloud_instance.db_password,
+
+            # Zookeeper
+            KylinProperties.ZOOKEEPER_HOST.value: self.cloud_instance.get_target_cluster_zk_host(cluster_num),
+
+            # S3 bucket path
+            KylinProperties.S3_BUCKET_PATH.value: self.cloud_instance.bucket_path,
+
+            # Spark Master
+            KylinProperties.SPARK_MASTER.value: self.cloud_instance.get_target_cluster_spark_master_host(cluster_num),
+        }
+        Utils.render_properties(params=params, cluster_num=cluster_num)
+
+    def upload_needed_files(self, tars: List, jars: List, scripts: List) -> None:
+        self.cloud_instance.upload_tars_to_s3(tars)
+        self.cloud_instance.upload_jars_to_s3(jars)
+        self.cloud_instance.upload_scripts_to_s3(scripts)
+
+    def check_needed_files(self, tars: List, jars: List, scripts: List) -> None:
+        self.cloud_instance.check_tars_on_s3(tars)
+        self.cloud_instance.check_jars_on_s3(jars)
+        self.cloud_instance.check_scripts_on_s3(scripts)
+
+    def upload_kylin_properties_to_clusters(self, cluster_nums: List) -> None:
+        for num in cluster_nums:
+            self.upload_kylin_properties(cluster_num=num)
+
+    def upload_kylin_properties(self, cluster_num: int = None) -> None:
+        # kylin.properties always need to upload
+        self.cloud_instance.upload_kylin_properties_to_s3(cluster_num=cluster_num)
diff --git a/constant/__init__.py b/constant/__init__.py
new file mode 100644
index 0000000..cce3aca
--- /dev/null
+++ b/constant/__init__.py
@@ -0,0 +1,16 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
diff --git a/constant/client.py b/constant/client.py
new file mode 100644
index 0000000..b8544a6
--- /dev/null
+++ b/constant/client.py
@@ -0,0 +1,24 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from enum import Enum
+
+
+class Client(Enum):
+    CLOUD_FORMATION = 'cloudformation'
+    EC2 = 'ec2'
+    RDS = 'rds'
diff --git a/constant/commands.py b/constant/commands.py
new file mode 100644
index 0000000..1a2d23b
--- /dev/null
+++ b/constant/commands.py
@@ -0,0 +1,49 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from enum import Enum
+
+
+class Commands(Enum):
+    STOP_PROMETHEUS_COMMAND = 'lsof -t -i:9090 | xargs -r kill -9'
+    ZKS_CFG_COMMAND = '''echo 'server.1={host1}:2888:3888\nserver.2={host2}:2888:3888\nserver.3={host3}:2888:3888' >> /home/ec2-user/hadoop/zookeeper/conf/zoo.cfg'''
+    # because zk need jdk env which created by root and env config in /root/.bash_profile
+    ZKS_START_COMMAND = 'sudo su && source ~/.bash_profile && ${ZOOKEEPER_HOME}/bin/zkServer.sh start'
+    ZKS_CHECK_CONFIGURED_COMMAND = '''grep -Fq "{host}:2888:3888" /home/ec2-user/hadoop/zookeeper/conf/zoo.cfg; echo $?'''
+    ZKS_CHECK_STARTED_COMMAND = 'sudo lsof -i:2181 >/dev/null 2>&1; echo $?'
+
+    START_PROMETHEUS_COMMAND = 'nohup /home/ec2-user/prometheus/prometheus --config.file=/home/ec2-user/prometheus/prometheus.yml >> /home/ec2-user/prometheus/nohup.out 2>&1 &'
+
+    # because SPARK need jdk env which created by root and env config in /root/.bash_profile
+    START_SPARK_MASTER_COMMAND = 'sudo su && source ~/.bash_profile && ${SPARK_HOME}/sbin/start-master.sh'
+
+    # Prometheus commands
+    PROMETHEUS_CFG_COMMAND = '''echo '  - job_name: "{node}"\n    static_configs:\n      - targets: ["{host}:9100"]' >> /home/ec2-user/prometheus/prometheus.yml '''
+    SPARK_DRIVER_METRIC_OF_KYLIN_COMMAND = """echo '  - job_name: "kylin-spark-driver-{node}"\n    metrics_path: /metrics/prometheus\n    static_configs:\n      - targets: ["{host}:4040"]' >> /home/ec2-user/prometheus/prometheus.yml """
+    PROMETHEUS_CFG_CHECK_COMMAND = '''grep -Fq "{node}" /home/ec2-user/prometheus/prometheus.yml; echo $?'''
+    PROMETHEUS_DELETE_CFG_COMMAND = '''sed -i "/{node}/,+2d" /home/ec2-user/prometheus/prometheus.yml'''
+    SPARK_DRIVER_METRIC_OF_KYLIN_DELETE_CFG_COMMAND = '''sed -i "/kylin-spark-driver-{node}/,+2d" /home/ec2-user/prometheus/prometheus.yml'''
+
+    # Spark metric commands
+    # Special COMMAND_TEMPLATE for spark metrics into prometheus
+    SPARK_MASTER_METRIC_COMMAND = """echo '  - job_name: "{node}"\n    metrics_path: /metrics/master/prometheus\n    static_configs:\n      - targets: ["{host}:8080"]' >> /home/ec2-user/prometheus/prometheus.yml """
+    SPARK_APPLICATIONS_METRIC_COMMAND = """echo '  - job_name: "{node}"\n    metrics_path: /metrics/applications/prometheus\n    static_configs:\n      - targets: ["{host}:8080"]' >> /home/ec2-user/prometheus/prometheus.yml """
+    SPARK_DRIVER_METRIC_COMMAND = """echo '  - job_name: "{node}"\n    metrics_path: /metrics/prometheus\n    static_configs:\n      - targets: ["{host}:4040"]' >> /home/ec2-user/prometheus/prometheus.yml """
+    SPARK_WORKER_METRIC_COMMAND = """echo '  - job_name: "{node}"\n    metrics_path: /metrics/prometheus\n    static_configs:\n      - targets: ["{host}:4041"]' >> /home/ec2-user/prometheus/prometheus.yml """
+    SPARK_EXECUTORS_METRIC_COMMAND = """echo '  - job_name: "{node}"\n    metrics_path: /metrics/executors/prometheus\n    static_configs:\n      - targets: ["{host}:4040"]' >> /home/ec2-user/prometheus/prometheus.yml """
+
+    SPARK_DECOMMISION_WORKER_COMMAND = 'sudo su && source ~/.bash_profile && ${SPARK_HOME}/sbin/decommission-worker.sh'
diff --git a/constant/config.py b/constant/config.py
new file mode 100644
index 0000000..b0353a7
--- /dev/null
+++ b/constant/config.py
@@ -0,0 +1,65 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from enum import Enum
+
+
+class Config(Enum):
+    # DEBUG params
+    CLOUD_ADDR = 'CLOUD_ADDR'
+
+    # Other params
+    AWS_REGION = 'AWS_REGION'
+    KYLIN_SCALE_UP_NODES = 'KYLIN_SCALE_UP_NODES'
+    KYLIN_SCALE_DOWN_NODES = 'KYLIN_SCALE_DOWN_NODES'
+    SPARK_WORKER_SCALE_UP_NODES = 'SPARK_WORKER_SCALE_UP_NODES'
+    SPARK_WORKER_SCALE_DOWN_NODES = 'SPARK_WORKER_SCALE_DOWN_NODES'
+    ENABLE_SOFT_AFFINITY = 'ENABLE_LOCAL_CACHE_SOFT_AFFINITY'
+    IAM = 'IAMRole'
+    KEY_PAIR = 'KeyName'
+    CLUSTER_INDEXES = 'CLUSTER_INDEXES'
+    CIDR_IP = 'CIDR_IP'
+
+    # Deploy params
+    DEPLOY_PLATFORM = 'DEPLOY_PLATFORM'
+
+    # Stack Names
+    VPC_STACK = 'VPC_STACK'
+    RDS_STACK = 'RDS_STACK'
+    STATIC_SERVICE_STACK = 'STATIC_SERVICE_STACK'
+    ZOOKEEPERS_STACK = 'ZOOKEEPERS_STACK'
+    KYLIN_STACK = 'KYLIN_STACK'
+    SPARK_MASTER_STACK = 'SPARK_MASTER_STACK'
+    SPARK_WORKER_STACK = 'SPARK_WORKER_STACK'
+
+    # Params
+    EC2_STATIC_SERVICES_PARAMS = 'EC2_STATIC_SERVICES_PARAMS'
+    EC2_KYLIN4_PARAMS = 'EC2_KYLIN4_PARAMS'
+    EC2_KYLIN4_SCALE_PARAMS = 'EC2_KYLIN4_SCALE_PARAMS'
+    EC2_VPC_PARAMS = 'EC2_VPC_PARAMS'
+    EC2_RDS_PARAMS = 'EC2_RDS_PARAMS'
+    EC2_ZOOKEEPERS_PARAMS = 'EC2_ZOOKEEPERS_PARAMS'
+    EC2_SPARK_MASTER_PARAMS = 'EC2_SPARK_MASTER_PARAMS'
+    EC2_SPARK_WORKER_PARAMS = 'EC2_SPARK_WORKER_PARAMS'
+    EC2_SPARK_SCALE_SLAVE_PARAMS = 'EC2_SPARK_SCALE_SLAVE_PARAMS'
+
+    # RDS Params
+    DB_IDENTIFIER = 'DB_IDENTIFIER'
+
+    DB_PORT = 'DB_PORT'
+    DB_USER = 'DB_USER'
+    DB_PASSWORD = 'DB_PASSWORD'
diff --git a/constant/deployment.py b/constant/deployment.py
new file mode 100644
index 0000000..e7ca930
--- /dev/null
+++ b/constant/deployment.py
@@ -0,0 +1,40 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from enum import Enum
+
+
+class DeployType(Enum):
+    DEPLOY = 'deploy'
+    LIST = 'list'
+    SCALE = 'scale'
+    DESTROY = 'destroy'
+
+
+class ScaleType(Enum):
+    UP = 'up'
+    DOWN = 'down'
+
+
+class NodeType(Enum):
+    KYLIN = 'kylin'
+    SPARK_WORKER = 'spark_worker'
+
+
+class Cluster(Enum):
+    ALL = 'all'
+    DEFAULT = 'default'
diff --git a/constant/kylin_properties_params.py b/constant/kylin_properties_params.py
new file mode 100644
index 0000000..d1e29f1
--- /dev/null
+++ b/constant/kylin_properties_params.py
@@ -0,0 +1,29 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from enum import Enum
+
+
+class KylinProperties(Enum):
+    DB_HOST = 'DB_HOST'
+    DB_PORT = 'DB_PORT'
+    DB_USER = 'DB_USER'
+    DB_PASSWORD = 'DB_PASSWORD'
+
+    S3_BUCKET_PATH = 'S3_BUCKET_PATH'
+    SPARK_MASTER = 'SPARK_MASTER'
+    ZOOKEEPER_HOST = 'ZOOKEEPER_HOST'
diff --git a/constant/path.py b/constant/path.py
new file mode 100644
index 0000000..833a31f
--- /dev/null
+++ b/constant/path.py
@@ -0,0 +1,34 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import os
+
+CUR_DIR = os.path.dirname(__file__)
+BACKUP_PATH = os.path.join(CUR_DIR, '..', 'backup')
+JARS_PATH = os.path.join(BACKUP_PATH, 'jars')
+SCRIPTS_PATH = os.path.join(BACKUP_PATH, 'scripts')
+TARS_PATH = os.path.join(BACKUP_PATH, 'tars')
+KYLIN_PROPERTIES_DIR = os.path.join(BACKUP_PATH, 'properties')
+
+KYLIN_PROPERTIES_TEMPLATE_DIR = os.path.join(KYLIN_PROPERTIES_DIR, '{cluster_num}')
+PROPERTIES_TEMPLATE_DIR = os.path.join(KYLIN_PROPERTIES_DIR, 'templates')
+
+TEMPLATE_OF_KYLIN_PROPERTIES = 'kylin.properties.template'
+RENDERED_FILE = '.rendered'
+
+ROOT_DIR = os.path.join(CUR_DIR, '..')
+TEMPLATES_PATH = os.path.join(ROOT_DIR, 'cloudformation_templates')
diff --git a/constant/server_mode.py b/constant/server_mode.py
new file mode 100644
index 0000000..e43c37a
--- /dev/null
+++ b/constant/server_mode.py
@@ -0,0 +1,24 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from enum import Enum
+
+
+class ServerMode(Enum):
+    ALL = 'all'
+    JOB = 'job'
+    QUERY = 'query'
diff --git a/constant/yaml_files.py b/constant/yaml_files.py
new file mode 100644
index 0000000..b0492d7
--- /dev/null
+++ b/constant/yaml_files.py
@@ -0,0 +1,43 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from enum import Enum
+
+
+class File(Enum):
+    CONFIG_YAML = 'kylin_configs.yaml'
+    VPC_YAML = 'ec2-or-emr-vpc.yaml'
+    RDS_YAML = 'ec2-cluster-rds.yaml'
+    STATIC_SERVICE_YAML = 'ec2-cluster-static-services.yaml'
+    ZOOKEEPERS_SERVICE_YAML = 'ec2-cluster-zk.yaml'
+    KYLIN4_YAML = 'ec2-cluster-kylin4.yaml'
+    KYLIN_SCALE_YAML = 'ec2-cluster-kylin4-template.yaml'
+    SPARK_MASTER_YAML = 'ec2-cluster-spark-master.yaml'
+    SPARK_WORKER_YAML = 'ec2-cluster-spark-slave.yaml'
+    SPARK_WORKER_SCALE_YAML = 'ec2-cluster-spark-slave-template.yaml'
+
+
+class Tar(Enum):
+    KYLIN = 'apache-kylin-{KYLIN_VERSION}-bin-spark3.tar.gz'
+    KYLIN_WITH_SOFT = 'apache-kylin-{KYLIN_VERSION}-bin-spark3-soft.tar.gz'
+    HIVE = 'apache-hive-{HIVE_VERSION}-bin.tar.gz'
+    HADOOP = 'hadoop-{HADOOP_VERSION}.tar.gz'
+    JDK = 'jdk-8u301-linux-x64.tar.gz'
+    NODE = 'node_exporter-{NODE_EXPORTER_VERSION}.linux-amd64.tar.gz'
+    PROMETHEUS = 'prometheus-{PROMETHEUS_VERSION}.linux-amd64.tar.gz'
+    SPARK = 'spark-{SPARK_VERSION}-bin-hadoop{HADOOP_VERSION!s:3.3s}.tgz'
+    ZOOKEEPER = 'zookeeper-{ZOOKEEPER_VERSION}.tar.gz'
diff --git a/constant/yaml_params.py b/constant/yaml_params.py
new file mode 100644
index 0000000..cfb7fcf
--- /dev/null
+++ b/constant/yaml_params.py
@@ -0,0 +1,92 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from enum import Enum
+
+
+class Params(Enum):
+    # global params
+    ASSOSICATED_PUBLIC_IP = 'ASSOSICATED_PUBLIC_IP'
+    ALWAYS_DESTROY_ALL = 'ALWAYS_DESTROY_ALL'
+    S3_URI = 'S3_URI'
+    INSTANCE_ID = 'IdOfInstance'
+    CLUSTER_NUM = 'ClusterNum'
+    CIDR_IP = 'CidrIp'
+    IS_SCALED = 'IsScaled'
+
+    # bucket path of stack
+    BUCKET_FULL_PATH = 'BucketFullPath'
+    BUCKET_PATH = 'BucketPath'
+
+    # Vpc params
+    VPC_ID = 'VpcId'
+    SUBNET_ID = 'SubnetId'
+    SECURITY_GROUP = 'SecurityGroupId'
+    INSTANCE_PROFILE = 'InstanceProfileId'
+    SUBNET_GROUP_NAME = 'SubnetGroupName'
+
+    # Rds params
+    DB_HOST = 'DbHost'
+
+    # Static services params
+    STATIC_SERVICES_PRIVATE_IP = 'StaticServicesNodePrivateIp'
+    STATIC_SERVICES_PUBLIC_IP = 'StaticServicesNodePublicIp'
+    STATIC_SERVICES_NAME = 'StaticServices Node'
+
+    # Zookeeper params
+    ZOOKEEPER_IP = 'ZookeeperNode0{num}PrivateIp'
+    ZOOKEEPER_PUB_IP = 'ZookeeperNode0{num}PublicIp'
+    ZOOKEEPER_INSTANCE_ID = 'IdOfInstanceZookeeper0{num}'
+    ZOOKEEPER_HOSTS = 'ZookeepersHost'
+    # Prometheus params of Zookeeper
+    ZOOKEEPER_NAME = 'Zookeeper_Node_0{num}'
+    ZOOKEEPER_NAME_IN_CLUSTER = 'Zookeeper_Node0{num}_In_Cluster_{cluster}'
+
+    # Kylin 4 params
+    KYLIN4_PUBLIC_IP = 'Kylin4Ec2InstancePublicIp'
+    KYLIN4_PRIVATE_IP = 'Kylin4Ec2InstancePrivateIp'
+
+    # Prometheus params of Kylin 4
+    KYLIN_NAME = 'Kylin_Node'
+    KYLIN_NAME_IN_CLUSTER = 'Kylin_Node_In_Cluster_{cluster}'
+    KYLIN_SPARK_DIRVER_IN_CLUSTER = 'Spark_Driver_Of_Kylin_Node_In_Cluster_{cluster}'
+    KYLIN_SCALE_STACK_NAME = 'ec2-kylin-scale-{num}'
+    KYLIN_SCALE_NODE_NAME_IN_CLUSTER = 'Kylin_Node_Scale_{num}_In_Cluster_{cluster}'
+    KYLIN_SCALE_TARGET_CLUSTER_STACK_NAME = 'ec2-kylin-scale-{num}-in-cluster-{cluster}'
+
+    # Spark master params
+    SPARK_MASTER_HOST = 'SparkMasterNodeHost'
+    SPARK_PUB_IP = 'SparkMasterEc2InstancePublicIp'
+
+    # Prometheus params of Spark master
+    SPARK_MASTER_NAME = 'Spark_Master_Node'
+    SPARK_MASTER_NAME_IN_CLUSTER = 'Spark_Master_Node_In_Cluster_{cluster}'
+    SPARK_MASTER_METRIC_IN_CLUSTER = 'Metric_Of_Spark_Master_Node_In_Cluster_{cluster}'
+
+    # Spark workers params
+    SPARK_WORKER_NUM = 'WorkerNum'
+    SPARK_WORKER_ID = 'IdOfInstanceSlave0{num}'
+    SPARK_WORKER_PRIVATE_IP = 'Slave0{num}Ec2InstancePrivateIp'
+    SPARK_WORKER_PUBLIC_IP = 'Slave0{num}Ec2InstancePublicIp'
+    SPARK_SCALED_WORKER_PRIVATE_IP = 'SlaveEc2InstancePrivateIp'
+    SPARK_SCALED_WORKER_PUBLIC_IP = 'SlaveEc2InstancePublicIp'
+
+    # Prometheus params of Spark workers
+    SPARK_WORKER_NAME = 'Spark_Worker_{num}'
+    SPARK_WORKER_NAME_IN_CLUSTER = 'Spark_Worker_{num}_In_Cluster_{cluster}'
+    SPARK_SCALE_WORKER_NAME_IN_CLUSTER = 'Spark_Worker_Node_Scale_{num}_In_Cluster_{cluster}'
+    SPARK_WORKER_SCALE_TARGET_CLUSTER_STACK_NAME = 'ec2-spark-worker-scale-{num}-in-cluster-{cluster}'
diff --git a/deploy.py b/deploy.py
new file mode 100644
index 0000000..3fdb5e0
--- /dev/null
+++ b/deploy.py
@@ -0,0 +1,98 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import argparse
+import logging.config
+
+from constant.deployment import DeployType, ScaleType, NodeType, Cluster
+
+
+def deploy_on_aws(deploy_type: str, scale_type: str, node_type: str, cluster: str = None) -> None:
+    from engine import Engine
+    aws_engine = Engine()
+    if not aws_engine.is_ec2_cluster:
+        msg = f'Now only supported platform: EC2, please check `DEPLOY_PLATFORM`.'
+        raise Exception(msg)
+
+    if deploy_type == DeployType.DEPLOY.value:
+        # init env for all clusters
+        aws_engine.init_env()
+
+        if cluster == Cluster.ALL.value:
+            aws_engine.launch_default_cluster()
+            aws_engine.launch_all_clusters()
+
+        if not cluster or cluster == Cluster.DEFAULT.value:
+            aws_engine.launch_default_cluster()
+
+        if cluster and cluster.isdigit():
+            aws_engine.launch_cluster(cluster_num=int(cluster))
+
+    elif deploy_type == DeployType.DESTROY.value:
+        if cluster == Cluster.ALL.value:
+            aws_engine.destroy_all_cluster()
+            aws_engine.destroy_default_cluster()
+            aws_engine.refresh_kylin_properties()
+            aws_engine.destroy_rds_and_vpc()
+
+        if not cluster or cluster == Cluster.DEFAULT.value:
+            aws_engine.destroy_default_cluster()
+            aws_engine.refresh_kylin_properties_in_default()
+
+        if cluster and cluster.isdigit():
+            aws_engine.destroy_cluster(cluster_num=int(cluster))
+            aws_engine.refresh_kylin_properties_in_clusters(cluster_nums=[int(cluster)])
+
+    elif deploy_type == DeployType.LIST.value:
+        aws_engine.list_alive_nodes()
+    elif deploy_type == DeployType.SCALE.value:
+        if not scale_type or not node_type:
+            msg = 'Invalid scale params, `scale-type`, `node-type` must be not None.'
+            raise Exception(msg)
+        aws_engine.scale_nodes(scale_type, node_type, cluster)
+
+
+if __name__ == '__main__':
+    logging.config.fileConfig('logging.ini')
+
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--type", required=False, default=DeployType.LIST.value, dest='type',
+                        choices=[e.value for e in DeployType],
+                        help="Use 'deploy' to create a cluster or 'destroy' to delete a cluster "
+                             "or 'list' to list alive nodes.")
+    parser.add_argument("--scale-type", required=False, dest='scale_type',
+                        choices=[e.value for e in ScaleType],
+                        help="This param must be used with '--type' and '--node-type' "
+                             "Use 'up' to scale up nodes or 'down' to scale down nodes. "
+                             "Node type will be in ['kylin', 'spark-worker'].")
+    parser.add_argument("--node-type", required=False, dest='node_type',
+                        choices=[e.value for e in NodeType],
+                        help="This param must be used with '--type' and '--scale-type' "
+                             "Use 'kylin' to scale up/down kylin nodes "
+                             "or 'spark-worker' to scale up/down spark_worker nodes. ")
+    # special choices for cluster
+    cluster_choices = [str(num) for num in range(1, 6)]
+    cluster_choices.append('all')
+    cluster_choices.append('default')
+    parser.add_argument("--cluster", required=False, dest='cluster', type=str,
+                        choices=cluster_choices,
+                        help="Use the `num` in the range `CLUSTER_INDEXES` to specify a cluster index "
+                             "which can mark what cluster nodes would be deployed or destroyed."
+                             "This param must be used with '--type', "
+                             "`default` is for to deploy or destroy nodes of `default` cluster.")
+    args = parser.parse_args()
+    deploy_on_aws(args.type, args.scale_type, args.node_type, args.cluster)
diff --git a/engine.py b/engine.py
new file mode 100644
index 0000000..a8e35d3
--- /dev/null
+++ b/engine.py
@@ -0,0 +1,135 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import logging
+import os
+from typing import List
+
+import yaml
+
+from constant.client import Client
+from constant.config import Config
+from constant.deployment import ScaleType, NodeType, Cluster
+from constant.yaml_files import File
+from engine_utils import EngineUtils
+
+logger = logging.getLogger(__name__)
+
+
+class Engine:
+
+    def __init__(self) -> None:
+        d = os.path.dirname(__file__)
+        with open(os.path.join(d, File.CONFIG_YAML.value)) as stream:
+            config = yaml.safe_load(stream)
+        self.config = config
+        self.is_ec2_cluster = self.config[Config.DEPLOY_PLATFORM.value] == Client.EC2.value
+        self.server_mode = None
+        self.engine_utils = EngineUtils(self.config)
+
+    def launch_default_cluster(self):
+        logger.info('First launch default Kylin Cluster.')
+        self.engine_utils.launch_default_cluster()
+        logger.info('Default Kylin Cluster already start successfully.')
+
+    def destroy_default_cluster(self):
+        logger.info('Start to destroy default Kylin Cluster.')
+        self.engine_utils.destroy_default_cluster()
+        logger.info('Destroy default Kylin Cluster successfully.')
+
+    def destroy_rds_and_vpc(self) -> None:
+        self.engine_utils.destroy_rds_and_vpc()
+
+    def list_alive_nodes(self) -> None:
+        logger.info('Ec2: list alive nodes.')
+        self.engine_utils.alive_nodes()
+        logger.info('Ec2: list alive nodes successfully.')
+
+    def scale_nodes(self, scale_type: str, node_type: str, cluster_num: int = None) -> None:
+        self.engine_utils.validate_scale_range()
+        if cluster_num == Cluster.ALL.value:
+            raise Exception('Current scale nodes not support to scale for `all` clusters.')
+
+        if cluster_num:
+            self.engine_utils.scale_nodes_in_cluster(
+                scale_type=scale_type,
+                node_type=node_type,
+                cluster_num=cluster_num
+            )
+        else:
+            self.engine_utils.scale_nodes(scale_type=scale_type, node_type=node_type)
+
+        if node_type == NodeType.KYLIN.value and scale_type == ScaleType.UP.value:
+            logger.info(f'Note: Current Kylin Node already scaled, please wait a moment to access it.')
+        logger.info(f"Current scaling {scale_type} {node_type} nodes "
+                    f"in {cluster_num if cluster_num else 'default'} cluster successfully.")
+
+    def launch_all_clusters(self) -> None:
+        logger.info(f'Current launch other clusters.')
+        self.engine_utils.launch_all_cluster()
+        logger.info(f'Current launch other clusters successfully.')
+
+    def launch_cluster(self, cluster_num: int) -> None:
+        logger.info(f'Current launch cluster {cluster_num}.')
+        self.engine_utils.launch_cluster(cluster_num=cluster_num)
+        logger.info(f'Current launch cluster {cluster_num} successfully.')
+        logger.info(f'Please note that access to Kylin may wait a moment.')
+
+    def destroy_all_cluster(self) -> None:
+        logger.info(f'Current destroy all scaled cluster nodes.')
+        self.engine_utils.destroy_all_cluster()
+        logger.info(f'Current destroy all scaled cluster nodes.')
+
+    def destroy_cluster(self, cluster_num: int) -> None:
+        logger.info(f'Current destroy launch cluster {cluster_num}.')
+        self.engine_utils.destroy_cluster(cluster_num=cluster_num)
+        logger.info(f'Current destroy cluster {cluster_num} successfully.')
+
+    def is_inited_env(self) -> bool:
+        try:
+            self.engine_utils.check_needed_files()
+            return True
+        except AssertionError:
+            return False
+
+    def init_env(self) -> None:
+        self._prepare_files()
+        self._prepare_services()
+
+    def _prepare_files(self) -> None:
+        self.engine_utils.validate_s3_bucket()
+        if self.is_inited_env():
+            logger.info('Env already inited, skip init again.')
+            return
+        self.engine_utils.download_tars()
+        self.engine_utils.download_jars()
+        self.engine_utils.upload_needed_files()
+        # check again
+        assert self.is_inited_env()
+
+    def _prepare_services(self) -> None:
+        # create vpc, rds and monitor node for whole cluster
+        self.engine_utils.prepare_for_cluster()
+
+    def refresh_kylin_properties(self) -> None:
+        self.engine_utils.refresh_kylin_properties()
+
+    def refresh_kylin_properties_in_clusters(self, cluster_nums: List[int]) -> None:
+        self.engine_utils.refresh_kylin_properties_in_clusters(cluster_nums)
+
+    def refresh_kylin_properties_in_default(self) -> None:
+        self.engine_utils.refresh_kylin_properties_in_default()
diff --git a/engine_utils.py b/engine_utils.py
new file mode 100644
index 0000000..823f42a
--- /dev/null
+++ b/engine_utils.py
@@ -0,0 +1,255 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import logging
+from typing import List
+
+from clouds.aws import AWS
+from constant.config import Config
+from constant.deployment import NodeType, ScaleType
+from constant.yaml_files import Tar
+from instances.kylin_utils import KylinUtils
+from utils import Utils
+
+logger = logging.getLogger(__name__)
+
+
+class EngineUtils:
+
+    def __init__(self, config):
+        self.aws = AWS(config)
+        self.config = config
+        
+    @property
+    def cloud_address(self) -> str:
+        return self.config[Config.CLOUD_ADDR.value]
+
+    def needed_tars(self) -> List:
+        jdk_package = Tar.JDK.value
+        kylin_package = Tar.KYLIN.value.format(KYLIN_VERSION=self.config['KYLIN_VERSION'])
+        if self.config[Config.ENABLE_SOFT_AFFINITY.value] == 'true':
+            kylin_package = Tar.KYLIN_WITH_SOFT.value.format(KYLIN_VERSION=self.config['KYLIN_VERSION'])
+        hive_package = Tar.HIVE.value.format(HIVE_VERSION=self.config['HIVE_VERSION'])
+        hadoop_package = Tar.HADOOP.value.format(HADOOP_VERSION=self.config['HADOOP_VERSION'])
+        node_exporter_package = Tar.NODE.value.format(NODE_EXPORTER_VERSION=self.config['NODE_EXPORTER_VERSION'])
+        prometheus_package = Tar.PROMETHEUS.value.format(PROMETHEUS_VERSION=self.config['PROMETHEUS_VERSION'])
+        spark_package = Tar.SPARK.value.format(SPARK_VERSION=self.config['SPARK_VERSION'],
+                                               HADOOP_VERSION=self.config['HADOOP_VERSION'])
+        zookeeper_package = Tar.ZOOKEEPER.value.format(ZOOKEEPER_VERSION=self.config['ZOOKEEPER_VERSION'])
+        packages = [jdk_package, kylin_package, hive_package, hadoop_package, node_exporter_package,
+                    prometheus_package, spark_package, zookeeper_package]
+        return packages
+
+    def needed_jars(self) -> List:
+        # FIXME: hard version of jars
+        jars = []
+        commons_configuration = 'commons-configuration-1.3.jar'
+        mysql_connector = 'mysql-connector-java-5.1.40.jar'
+        jars.append(commons_configuration)
+        jars.append(mysql_connector)
+        if self.config[Config.ENABLE_SOFT_AFFINITY.value] == 'true':
+            kylin_soft_affinity_cache = 'kylin-soft-affinity-cache-4.0.0-SNAPSHOT.jar'
+            alluxio_client = 'alluxio-2.6.1-client.jar'
+            jars.append(kylin_soft_affinity_cache)
+            jars.append(alluxio_client)
+        return jars
+
+    @staticmethod
+    def needed_scripts() -> List:
+        kylin = 'prepare-ec2-env-for-kylin4.sh'
+        spark_master = 'prepare-ec2-env-for-spark-master.sh'
+        spark_slave = 'prepare-ec2-env-for-spark-slave.sh'
+        static_services = 'prepare-ec2-env-for-static-services.sh'
+        zookeeper = 'prepare-ec2-env-for-zk.sh'
+        return [kylin, spark_master, spark_slave, static_services, zookeeper]
+
+    def launch_default_cluster(self):
+        cloud_addr = self.get_kylin_address()
+        # check kylin status
+        KylinUtils.is_kylin_accessible(cloud_addr)
+
+    def destroy_default_cluster(self):
+        # Before destroy default cluster, scale down all the scaled nodes
+        self.scale_nodes(scale_type=ScaleType.DOWN.value, node_type=NodeType.KYLIN.value)
+        self.scale_nodes(scale_type=ScaleType.DOWN.value, node_type=NodeType.SPARK_WORKER.value)
+
+        self.aws.destroy_default_cluster()
+
+    def alive_nodes(self):
+        self.aws.alive_nodes()
+
+    def scale_nodes(self, scale_type: str, node_type: str) -> None:
+        logger.info(f'Current scaling {scale_type} {node_type} nodes.')
+        self.validate_scale_type(scale_type)
+
+        if scale_type == ScaleType.UP.value:
+            assert self.is_cluster_ready() is True, 'Cluster nodes must be ready.'
+            self.aws.scale_up(node_type=node_type)
+            self.aws.after_scale_up(node_type=node_type)
+
+        elif scale_type == ScaleType.DOWN.value:
+            self.aws.after_scale_down(node_type=node_type)
+            self.aws.scale_down(node_type=node_type)
+
+        self.aws.restart_prometheus_server()
+
+    def scale_nodes_in_cluster(
+            self,
+            scale_type: str,
+            node_type: str,
+            cluster_num: int,
+            is_destroy: bool = False) -> None:
+        logger.info(f"Current scaling {scale_type} {node_type} nodes "
+                    f"in cluster {cluster_num if cluster_num else 'default'}.")
+        self.validate_scale_type(scale_type)
+
+        if scale_type == ScaleType.UP.value:
+            assert self.is_target_cluster_ready(cluster_num) is True, 'Cluster nodes must be ready.'
+            self.aws.scale_up(node_type=node_type, cluster_num=cluster_num, is_destroy=is_destroy)
+            self.aws.after_scale_up(node_type=node_type, cluster_num=cluster_num)
+        else:
+            self.aws.after_scale_down(node_type=node_type, cluster_num=cluster_num)
+            self.aws.scale_down(node_type=node_type, cluster_num=cluster_num, is_destroy=is_destroy)
+
+        self.aws.restart_prometheus_server()
+
+    def prepare_for_cluster(self) -> None:
+        # create vpc, rds and monitor node for whole cluster
+        if not self.is_prepared_for_scale_cluster():
+            self.aws.prepare_for_whole_cluster()
+
+    def launch_all_cluster(self) -> None:
+        self.aws.launch_clusters()
+        self.aws.after_launch_clusters()
+        self.aws.restart_prometheus_server()
+
+    def launch_cluster(self, cluster_num: int) -> None:
+        self.aws.launch_clusters(cluster_nums=[cluster_num])
+        self.aws.after_launch_clusters(cluster_nums=[cluster_num])
+        self.aws.restart_prometheus_server()
+
+    def destroy_all_cluster(self) -> None:
+        # Before destroy all nodes, scale down the nodes in the cluster
+        cluster_nums: List[int] = self.aws.generate_scaled_cluster_nums()
+        for num in cluster_nums:
+            self.scale_nodes_in_cluster(
+                scale_type=ScaleType.DOWN.value,
+                node_type=NodeType.KYLIN.value,
+                cluster_num=num, is_destroy=True)
+            self.scale_nodes_in_cluster(
+                scale_type=ScaleType.DOWN.value,
+                node_type=NodeType.SPARK_WORKER.value,
+                cluster_num=num, is_destroy=True)
+
+        self.aws.after_destroy_clusters(cluster_nums=cluster_nums)
+        self.aws.destroy_clusters(cluster_nums=cluster_nums)
+        self.aws.restart_prometheus_server()
+
+    def destroy_cluster(self, cluster_num: int) -> None:
+        self.scale_nodes_in_cluster(
+            scale_type=ScaleType.DOWN.value,
+            node_type=NodeType.KYLIN.value,
+            cluster_num=cluster_num,
+            is_destroy=True)
+
+        self.aws.after_destroy_clusters(cluster_nums=[cluster_num])
+        self.aws.destroy_clusters(cluster_nums=[cluster_num])
+        self.aws.restart_prometheus_server()
+
+    def destroy_rds_and_vpc(self) -> None:
+        self.aws.destroy_rds_and_vpc()
+
+    def is_cluster_ready(self) -> bool:
+        if self.cloud_address:
+            return True
+        return self.aws.is_cluster_ready
+
+    def is_target_cluster_ready(self, cluster_num: int) -> bool:
+        return self.aws.is_target_cluster_instances_ready(cluster_num)
+
+    def is_prepared_for_scale_cluster(self) -> bool:
+        return self.aws.is_prepared_for_scale_cluster
+
+    def get_kylin_address(self) -> str:
+        kylin_address = self.cloud_address
+        if not kylin_address:
+            kylin_address = self.aws.get_kylin_address()
+        assert kylin_address, f'kylin address is None, please check.'
+        return kylin_address
+
+    def download_tars(self) -> None:
+        logger.info("Downloading packages.")
+        packages = self.needed_tars()
+        for package in packages:
+            Utils.download_tar(filename=package)
+
+    def download_jars(self) -> None:
+        logger.info("Downloading jars.")
+        jars = self.needed_jars()
+        for jar in jars:
+            Utils.download_jar(jar)
+        if self.config[Config.ENABLE_SOFT_AFFINITY.value] == 'true':
+            assert Utils.files_in_jars() == 4, f"Needed jars must be 4, not {Utils.files_in_jars()}, " \
+                                         f"which contains {jars}."
+        else:
+            assert Utils.files_in_jars() == 2, f"Needed jars must be 2, not {Utils.files_in_jars()}, " \
+                                         f"which contains {jars}."
+
+    def upload_needed_files(self) -> None:
+        logger.info("Start to uploading tars.")
+        self.aws.upload_needed_files(self.needed_tars(), self.needed_jars(), self.needed_scripts())
+        logger.info("Uploaded tars successfully.")
+
+    def check_needed_files(self) -> None:
+        self.aws.check_needed_files(self.needed_tars(), self.needed_jars(), self.needed_scripts())
+
+    def upload_kylin_properties(self, cluster_num: int = None) -> None:
+        logger.info(f"Start to uploading kylin.properties for cluster {cluster_num if cluster_num else 'default'}.")
+        self.aws.upload_kylin_properties(cluster_num=cluster_num)
+        logger.info(f"Uploaded kylin.properties for cluster {cluster_num if cluster_num else 'default'}.")
+
+    def validate_s3_bucket(self) -> None:
+        self.aws.validate_s3_bucket()
+
+    def validate_scale_type(self, scale_type: str) -> None:
+        self.aws.validate_scale_type(scale_type=scale_type)
+
+    def validate_scale_range(self) -> None:
+        self.aws.validate_scale_range()
+
+    def init_kylin_properties(self, cluster_num: int = None) -> None:
+        logger.info(f"Start to init kylin properties for cluster {cluster_num if cluster_num else 'default'}.")
+        self.aws.init_kylin_properties(cluster_num)
+        logger.info(f"Inited kylin properties for cluster {cluster_num if cluster_num else 'default'}.")
+
+    @staticmethod
+    def refresh_kylin_properties() -> None:
+        logger.info('Start to refresh kylin.poperties in `kylin-tpch/properties/default`.')
+        Utils.refresh_kylin_properties()
+        logger.info('Refresh kylin.poperties in `kylin-tpch/properties/default` successfully.')
+
+    @staticmethod
+    def refresh_kylin_properties_in_clusters(cluster_nums: List) -> None:
+        logger.info('Start to refresh kylin.poperties.')
+        Utils.refresh_kylin_properties_in_clusters(cluster_nums=cluster_nums)
+        logger.info('Refresh kylin.poperties successfully.')
+
+    @staticmethod
+    def refresh_kylin_properties_in_default() -> None:
+        logger.info('Start to refresh kylin.poperties.')
+        Utils.refresh_kylin_properties_in_default()
+        logger.info('Refresh kylin.poperties successfully.')
diff --git a/images/accesskey.png b/images/accesskey.png
new file mode 100644
index 0000000..24664ac
Binary files /dev/null and b/images/accesskey.png differ
diff --git a/images/accesskey2.png b/images/accesskey2.png
new file mode 100644
index 0000000..22f7744
Binary files /dev/null and b/images/accesskey2.png differ
diff --git a/images/directory.png b/images/directory.png
new file mode 100644
index 0000000..22ebf22
Binary files /dev/null and b/images/directory.png differ
diff --git a/images/granfana2.png b/images/granfana2.png
new file mode 100644
index 0000000..e25cc99
Binary files /dev/null and b/images/granfana2.png differ
diff --git a/images/granfana3.png b/images/granfana3.png
new file mode 100644
index 0000000..f069ea2
Binary files /dev/null and b/images/granfana3.png differ
diff --git a/images/granfanadashboard.png b/images/granfanadashboard.png
new file mode 100644
index 0000000..26530d4
Binary files /dev/null and b/images/granfanadashboard.png differ
diff --git a/images/granfanadatasource.png b/images/granfanadatasource.png
new file mode 100644
index 0000000..9cc4c6e
Binary files /dev/null and b/images/granfanadatasource.png differ
diff --git a/images/granfanaeditpanel.png b/images/granfanaeditpanel.png
new file mode 100644
index 0000000..3ca9f0e
Binary files /dev/null and b/images/granfanaeditpanel.png differ
diff --git a/images/granfanapanel.png b/images/granfanapanel.png
new file mode 100644
index 0000000..50c7878
Binary files /dev/null and b/images/granfanapanel.png differ
diff --git a/images/jars.png b/images/jars.png
new file mode 100644
index 0000000..4f25645
Binary files /dev/null and b/images/jars.png differ
diff --git a/images/keypair.png b/images/keypair.png
new file mode 100644
index 0000000..4049a0b
Binary files /dev/null and b/images/keypair.png differ
diff --git a/images/kylinproperties.png b/images/kylinproperties.png
new file mode 100644
index 0000000..e246d6a
Binary files /dev/null and b/images/kylinproperties.png differ
diff --git a/images/newpanel.png b/images/newpanel.png
new file mode 100644
index 0000000..324fda4
Binary files /dev/null and b/images/newpanel.png differ
diff --git a/images/prometheusmetrics.png b/images/prometheusmetrics.png
new file mode 100644
index 0000000..595ae61
Binary files /dev/null and b/images/prometheusmetrics.png differ
diff --git a/images/prometheusquery.png b/images/prometheusquery.png
new file mode 100644
index 0000000..23a2ede
Binary files /dev/null and b/images/prometheusquery.png differ
diff --git a/images/renderedkylinproperties.png b/images/renderedkylinproperties.png
new file mode 100644
index 0000000..34f11fb
Binary files /dev/null and b/images/renderedkylinproperties.png differ
diff --git a/images/scripts.png b/images/scripts.png
new file mode 100644
index 0000000..ddf16a4
Binary files /dev/null and b/images/scripts.png differ
diff --git a/images/sparkmetrics.png b/images/sparkmetrics.png
new file mode 100644
index 0000000..7257bd1
Binary files /dev/null and b/images/sparkmetrics.png differ
diff --git a/images/staticserviceparam.png b/images/staticserviceparam.png
new file mode 100644
index 0000000..6461507
Binary files /dev/null and b/images/staticserviceparam.png differ
diff --git a/images/structure.png b/images/structure.png
new file mode 100644
index 0000000..c48b1c0
Binary files /dev/null and b/images/structure.png differ
diff --git a/images/tars.jpg b/images/tars.jpg
new file mode 100644
index 0000000..229db2c
Binary files /dev/null and b/images/tars.jpg differ
diff --git a/images/tars.png b/images/tars.png
new file mode 100644
index 0000000..8019d1b
Binary files /dev/null and b/images/tars.png differ
diff --git a/instances/__init__.py b/instances/__init__.py
new file mode 100644
index 0000000..cce3aca
--- /dev/null
+++ b/instances/__init__.py
@@ -0,0 +1,16 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
diff --git a/instances/aws_instance.py b/instances/aws_instance.py
new file mode 100644
index 0000000..cc8b822
--- /dev/null
+++ b/instances/aws_instance.py
@@ -0,0 +1,2163 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import logging
+import os
+import re
+import time
+from ast import literal_eval
+from typing import Optional, Dict, List, Tuple
+
+import boto3
+import botocore
+from botocore.exceptions import ClientError, WaiterError, ParamValidationError
+
+from constant.client import Client
+from constant.commands import Commands
+from constant.config import Config
+from constant.deployment import NodeType, Cluster
+from constant.path import JARS_PATH, TARS_PATH, SCRIPTS_PATH, KYLIN_PROPERTIES_TEMPLATE_DIR
+from constant.yaml_files import File
+from constant.yaml_params import Params
+from utils import Utils
+
+logger = logging.getLogger(__name__)
+
+
+class AWSInstance:
+
+    def __init__(self, config):
+        self.config = config
+        # global params
+        self.vpc_id = None
+        self.instance_profile = None
+        self.security_group = None
+        self.subnet_group = None
+        self.subnet_id = None
+        self.db_host = None
+
+    @property
+    def is_associated_public_ip(self) -> bool:
+        return self.config[Params.ASSOSICATED_PUBLIC_IP.value] == 'true'
+
+    @property
+    def scaled_spark_workers(self) -> Optional[Tuple]:
+        if self.config[Config.SPARK_WORKER_SCALE_UP_NODES.value]:
+            return literal_eval(self.config[Config.SPARK_WORKER_SCALE_UP_NODES.value])
+        return ()
+
+    @property
+    def scaled_down_spark_workers(self) -> Optional[Tuple]:
+        if self.config[Config.SPARK_WORKER_SCALE_UP_NODES.value]:
+            return literal_eval(self.config[Config.SPARK_WORKER_SCALE_UP_NODES.value])
+        return ()
+
+    @property
+    def scaled_spark_workers_stacks(self) -> List:
+        return self.scaled_target_spark_workers_stacks()
+
+    def scaled_target_spark_workers_stacks(self, cluster_num: int = None) -> List:
+        return [
+            Params.SPARK_WORKER_SCALE_TARGET_CLUSTER_STACK_NAME.value.format(
+                num=i, cluster=cluster_num if cluster_num else 'default')
+            for i in Utils.generate_nodes(self.scaled_spark_workers)]
+
+    @property
+    def scaled_kylin_nodes(self) -> Optional[Tuple]:
+        if self.config[Config.KYLIN_SCALE_UP_NODES.value]:
+            return literal_eval(self.config[Config.KYLIN_SCALE_UP_NODES.value])
+        return ()
+
+    @property
+    def scaled_down_kylin_nodes(self) -> Optional[Tuple]:
+        if self.config[Config.KYLIN_SCALE_UP_NODES.value]:
+            return literal_eval(self.config[Config.KYLIN_SCALE_DOWN_NODES.value])
+        return ()
+
+    @property
+    def scaled_kylin_stacks(self) -> List:
+        return self.scaled_target_kylin_stacks()
+
+    def scaled_target_kylin_stacks(self, cluster_num: int = None) -> List:
+        return [
+            Params.KYLIN_SCALE_TARGET_CLUSTER_STACK_NAME.value.format(
+                num=i, cluster=cluster_num if cluster_num else 'default')
+            for i in Utils.generate_nodes(self.scaled_kylin_nodes)]
+
+    @property
+    def region(self) -> str:
+        return self.config[Config.AWS_REGION.value]
+
+    @property
+    def cf_client(self):
+        return boto3.client(Client.CLOUD_FORMATION.value, region_name=self.region)
+
+    @property
+    def rds_client(self):
+        return boto3.client(Client.RDS.value, region_name=self.region)
+
+    @property
+    def ec2_client(self):
+        return boto3.client(Client.EC2.value, region_name=self.region)
+
+    @property
+    def ssm_client(self):
+        return boto3.client('ssm', region_name=self.region)
+
+    @property
+    def s3_client(self):
+        return boto3.client('s3', region_name=self.region)
+
+    @property
+    def iam_client(self):
+        return boto3.client('iam', region_name=self.region)
+
+    @property
+    def create_complete_waiter(self):
+        return self.cf_client.get_waiter('stack_create_complete')
+
+    @property
+    def delete_complete_waiter(self):
+        return self.cf_client.get_waiter('stack_delete_complete')
+
+    @property
+    def exists_waiter(self):
+        return self.cf_client.get_waiter('stack_exists')
+
+    @property
+    def db_port(self) -> str:
+        return self.config[Config.DB_PORT.value]
+
+    @property
+    def db_user(self) -> str:
+        return self.config[Config.DB_USER.value]
+
+    @property
+    def db_password(self) -> str:
+        return self.config[Config.DB_PASSWORD.value]
+
+    @property
+    def db_identifier(self) -> str:
+        return self.config[Config.DB_IDENTIFIER.value]
+
+    @property
+    def vpc_stack_name(self) -> str:
+        return self.config[Config.VPC_STACK.value]
+
+    @property
+    def rds_stack_name(self) -> str:
+        return self.config[Config.RDS_STACK.value]
+
+    @property
+    def static_service_stack_name(self) -> str:
+        return self.config[Config.STATIC_SERVICE_STACK.value]
+
+    @property
+    def zk_stack_name(self) -> str:
+        return self.config[Config.ZOOKEEPERS_STACK.value]
+
+    @property
+    def kylin_stack_name(self) -> str:
+        return self.config[Config.KYLIN_STACK.value]
+
+    @property
+    def spark_master_stack_name(self) -> str:
+        return self.config[Config.SPARK_MASTER_STACK.value]
+
+    @property
+    def spark_slave_stack_name(self) -> str:
+        return self.config[Config.SPARK_WORKER_STACK.value]
+
+    @property
+    def path_of_vpc_stack(self) -> str:
+        return Utils.full_path_of_yaml(File.VPC_YAML.value)
+
+    @property
+    def path_of_rds_stack(self) -> str:
+        return Utils.full_path_of_yaml(File.RDS_YAML.value)
+
+    @property
+    def path_of_static_service_stack(self) -> str:
+        return Utils.full_path_of_yaml(File.STATIC_SERVICE_YAML.value)
+
+    @property
+    def path_of_zk_stack(self) -> str:
+        return Utils.full_path_of_yaml(File.ZOOKEEPERS_SERVICE_YAML.value)
+
+    @property
+    def path_of_kylin_stack(self) -> str:
+        return Utils.full_path_of_yaml(File.KYLIN4_YAML.value)
+
+    @property
+    def path_of_kylin_scale_stack(self) -> str:
+        return Utils.full_path_of_yaml(File.KYLIN_SCALE_YAML.value)
+
+    @property
+    def path_of_spark_master_stack(self) -> str:
+        return Utils.full_path_of_yaml(File.SPARK_MASTER_YAML.value)
+
+    @property
+    def path_of_spark_slave_stack(self) -> str:
+        return Utils.full_path_of_yaml(File.SPARK_WORKER_YAML.value)
+
+    @property
+    def path_of_spark_slave_scaled_stack(self) -> str:
+        return Utils.full_path_of_yaml(File.SPARK_WORKER_SCALE_YAML.value)
+
+    @property
+    def s3_path_match(self) -> re.Match:
+        original_path: str = self.config[Params.S3_URI.value]
+        match = re.match(pattern=r'^s3://([^/]+)/(.*?([^/]+)/?)$', string=original_path)
+        if not match:
+            raise Exception(f"Invalid S3 bucket path: {original_path}, please check.")
+        return match
+
+    @property
+    def is_valid_scaled_range(self) -> bool:
+        return self.is_valid_scaled_range_of_kylin_nodes and self.is_valid_scaled_range_of_spark_worker_nodes
+
+    @property
+    def is_valid_scaled_range_of_kylin_nodes(self) -> bool:
+        return set(self.scaled_down_kylin_nodes).issubset(self.scaled_kylin_nodes)
+
+    @property
+    def is_valid_scaled_range_of_spark_worker_nodes(self) -> bool:
+        return set(self.scaled_down_spark_workers).issubset(self.scaled_spark_workers)
+
+    @property
+    def bucket_full_path(self) -> str:
+        full_path = self.s3_path_match.group(0)
+        if full_path.endswith('/'):
+            full_path = full_path.rstrip('/')
+        return full_path
+
+    @property
+    def bucket_path(self) -> str:
+        # remove the prefix of 's3:/'
+        path = self.bucket_full_path[len('s3:/'):]
+        return path
+
+    @property
+    def bucket(self) -> str:
+        # get the bucket
+        return self.s3_path_match.group(1)
+
+    @property
+    def bucket_dir(self) -> str:
+        # Note: this dir is for
+        directory = self.s3_path_match.group(2)
+        if directory.endswith('/'):
+            return directory
+        return directory + '/'
+
+    @property
+    def bucket_tars_dir(self) -> str:
+        return self.bucket_dir + 'tar/'
+
+    @property
+    def bucket_scripts_dir(self) -> str:
+        return self.bucket_dir + 'scripts/'
+
+    @property
+    def bucket_kylin_properties_of_default_cluster_dir(self) -> str:
+        return self.bucket_dir + 'properties/default/'
+
+    @property
+    def bucket_kylin_properties_of_other_cluster_dir(self) -> str:
+        return self.bucket_dir + 'properties/{cluster_num}/'
+
+    @property
+    def bucket_jars_dir(self) -> str:
+        return self.bucket_dir + 'jars/'
+
+    @property
+    def iam_role(self) -> str:
+        return self.config[Config.IAM.value]
+
+    @property
+    def key_pair(self) -> str:
+        return self.config[Config.KEY_PAIR.value]
+
+    @property
+    def instance_id_of_static_services(self) -> str:
+        return self.get_instance_id(self.static_service_stack_name)
+
+    @property
+    def cidr_ip(self) -> str:
+        return self.config[Config.CIDR_IP.value]
+
+    def get_vpc_id(self) -> str:
+        if not self.vpc_id:
+            self.vpc_id = self.get_specify_resource_from_output(
+                self.vpc_stack_name, Params.VPC_ID.value
+            )
+            assert self.vpc_id, f'vpc id must not be empty or None.'
+        return self.vpc_id
+
+    def get_instance_profile(self) -> str:
+        if not self.instance_profile:
+            self.instance_profile = self.get_specify_resource_from_output(
+                self.vpc_stack_name, Params.INSTANCE_PROFILE.value
+            )
+        return self.instance_profile
+
+    def get_subnet_id(self) -> str:
+        if not self.subnet_id:
+            self.subnet_id = self.get_specify_resource_from_output(self.vpc_stack_name, Params.SUBNET_ID.value)
+            assert self.subnet_id, 'subnet id must not be empty or None.'
+        return self.subnet_id
+
+    def get_subnet_group(self) -> str:
+        if not self.subnet_group:
+            self.subnet_group = self.get_specify_resource_from_output(
+                self.vpc_stack_name, Params.SUBNET_GROUP_NAME.value
+            )
+            assert self.subnet_group, 'subnet group must not be empty or None.'
+        return self.subnet_group
+
+    def get_db_host(self) -> str:
+        if not self.db_host:
+            self.db_host = self.get_rds_describe()['Endpoint']['Address']
+            assert self.db_host, 'db_host must not be empty or None.'
+        return self.db_host
+
+    def get_security_group_id(self) -> str:
+        if not self.security_group:
+            self.security_group = self.get_specify_resource_from_output(
+                self.vpc_stack_name, Params.SECURITY_GROUP.value
+            )
+            assert self.security_group, f'security_group_id must not be empty or None.'
+        return self.security_group
+
+    def get_instance_id(self, stack_name: str) -> str:
+        if not self.is_stack_complete(stack_name):
+            return ""
+        return self.get_specify_resource_from_output(stack_name, Params.INSTANCE_ID.value)
+
+    def get_instance_ids_of_slaves_stack(self) -> List:
+        return [
+            self.get_specify_resource_from_output(
+                self.spark_slave_stack_name, f'IdOfInstanceOfSlave0{i}'
+            )
+            for i in range(1, 4)
+        ]
+
+    # ============ VPC Services Start ============
+    def create_vpc_stack(self) -> Optional[Dict]:
+        if self.is_stack_complete(self.vpc_stack_name):
+            return
+        params: Dict = self.config[Config.EC2_VPC_PARAMS.value]
+        params[Params.CIDR_IP.value] = self.cidr_ip
+        resp = self.create_stack(
+            stack_name=self.vpc_stack_name,
+            file_path=self.path_of_vpc_stack,
+            params=params,
+            is_capability=True
+        )
+        return resp
+
+    def terminate_vpc_stack(self) -> Optional[Dict]:
+        resp = self.terminate_stack_by_name(self.vpc_stack_name)
+        # Make sure that vpc stack deleted successfully.
+        assert self.is_stack_deleted_complete(self.vpc_stack_name), \
+            f'{self.vpc_stack_name} deleted failed, please check.'
+        return resp
+
+    # ============ VPC Services End ============
+
+    # ============ RDS Services Start ============
+    def get_rds_describe(self) -> Optional[Dict]:
+        if not self.db_identifier:
+            raise Exception(f'{Config.DB_IDENTIFIER.value} must not be empty or None.')
+        if not self.is_rds_exists():
+            return
+        describe_rds: Dict = self.rds_client.describe_db_instances(DBInstanceIdentifier=self.db_identifier)
+        db_instances: List = describe_rds['DBInstances']
+        assert len(db_instances) == 1, f'the identifier of RDS must exist only one.'
+        return db_instances[0]
+
+    def is_rds_exists(self) -> bool:
+        try:
+            self.rds_client.describe_db_instances(DBInstanceIdentifier=self.db_identifier)
+        except self.rds_client.exceptions.DBInstanceNotFoundFault as ex:
+            logger.warning(ex.response['Error']['Message'])
+            logger.info(f'Now creating {self.db_identifier}.')
+            return False
+        return True
+
+    def create_rds_stack(self) -> Optional[Dict]:
+        if self.is_stack_complete(self.rds_stack_name):
+            return
+        if self.is_rds_exists():
+            logger.warning(f'db {self.db_identifier} already exists.')
+            return
+        params: Dict = self.config[Config.EC2_RDS_PARAMS.value]
+        # update needed params
+        params[Params.SUBNET_GROUP_NAME.value] = self.get_subnet_group()
+        params[Params.SECURITY_GROUP.value] = self.get_security_group_id()
+        resp = self.create_stack(
+            stack_name=self.rds_stack_name,
+            file_path=self.path_of_rds_stack,
+            params=params,
+        )
+        # Make sure that rds create successfully.
+        assert self.is_stack_complete(self.rds_stack_name), f'Rds {self.db_identifier} create failed, please check.'
+        return resp
+
+    def terminate_rds_stack(self) -> Optional[Dict]:
+        # Note: terminated rds will not delete it, user can delete db manually.
+        resp = self.terminate_stack_by_name(self.rds_stack_name)
+        return resp
+
+    # ============ RDS Services End ============
+
+    # ============ Static Services Start ============
+    def create_static_service_stack(self) -> Optional[Dict]:
+        if not self.is_rds_ready():
+            msg = f'rds {self.db_identifier} is not ready, please check.'
+            logger.warning(msg)
+            raise Exception(msg)
+
+        if self.is_stack_complete(self.static_service_stack_name):
+            return
+        params: Dict = self.config[Config.EC2_STATIC_SERVICES_PARAMS.value]
+        # update needed params
+        params = self.update_basic_params(params)
+        params[Params.DB_HOST.value] = self.get_db_host()
+
+        resp = self.create_stack(
+            stack_name=self.static_service_stack_name,
+            file_path=self.path_of_static_service_stack,
+            params=params
+        )
+        return resp
+
+    def terminate_static_service_stack(self) -> Optional[Dict]:
+        resp = self.terminate_stack_by_name(self.static_service_stack_name)
+        return resp
+
+    def get_static_services_instance_id(self) -> str:
+        if not self.is_stack_complete(self.static_service_stack_name):
+            return ''
+        return self.get_specify_resource_from_output(self.static_service_stack_name, Params.INSTANCE_ID.value)
+
+    def get_static_services_private_ip(self) -> str:
+        if not self.is_stack_complete(self.static_service_stack_name):
+            return ''
+        return self.get_specify_resource_from_output(
+            self.static_service_stack_name, Params.STATIC_SERVICES_PRIVATE_IP.value)
+
+    def get_static_services_public_ip(self) -> str:
+        if not self.is_stack_complete(self.static_service_stack_name):
+            return ''
+        if not self.is_associated_public_ip:
+            logger.warning('Current static services was associated to a public ip.')
+            return ''
+        return self.get_specify_resource_from_output(
+            self.static_service_stack_name, Params.STATIC_SERVICES_PUBLIC_IP.value)
+
+    def get_static_services_basic_msg(self) -> str:
+        if not self.is_stack_complete(self.static_service_stack_name):
+            return ''
+        res = Params.STATIC_SERVICES_NAME.value + '\t' \
+              + self.instance_id_of_static_services + '\t' \
+              + self.get_static_services_private_ip() + '\t' \
+              + self.get_static_services_public_ip()
+        return res
+
+    # ============ Static Services End ============
+
+    # ============ Zookeeper Services Start ============
+    def get_instance_ids_of_zks_stack(self) -> List:
+        return self.get_instance_ids_of_zks_stack_by_name(self.zk_stack_name)
+
+    def get_instance_ids_of_zks_stack_by_name(self, zk_stack_name: str) -> List:
+        if not self.is_stack_complete(zk_stack_name):
+            return []
+
+        return [
+            self.get_specify_resource_from_output(
+                zk_stack_name, Params.ZOOKEEPER_INSTANCE_ID.value.format(num=i)
+            )
+            for i in range(1, 4)
+        ]
+
+    def get_instance_ips_of_zks_stack(self) -> List:
+        return self.get_instance_ips_of_zks_stack_by_name(self.zk_stack_name)
+
+    def get_instance_ips_of_zks_stack_by_name(self, zk_stack_name: str) -> List:
+        if not self.is_stack_complete(zk_stack_name):
+            return []
+        return [self.get_specify_resource_from_output(
+            zk_stack_name, Params.ZOOKEEPER_IP.value.format(num=i))
+            for i in range(1, 4)
+        ]
+
+    def create_zk_stack(self) -> Optional[Dict]:
+        return self.create_zk_stack_by_cluster()
+
+    def create_zk_stack_by_cluster(self, cluster_num: int = None) -> Optional[Dict]:
+        if cluster_num:
+            zk_stack_name = self.generate_stack_of_scaled_cluster_for_zk(cluster_num)
+        else:
+            zk_stack_name = self.zk_stack_name
+
+        if self.is_stack_complete(zk_stack_name):
+            return
+        params: Dict = self.config[Config.EC2_ZOOKEEPERS_PARAMS.value]
+        # update needed params
+        params = self.update_basic_params(params)
+
+        resp = self.create_stack(
+            stack_name=zk_stack_name,
+            file_path=self.path_of_zk_stack,
+            params=params
+        )
+        assert self.is_stack_complete(zk_stack_name), f'{zk_stack_name} create failed, please check.'
+        return resp
+
+    def terminate_zk_stack(self) -> Optional[Dict]:
+        resp = self.terminate_stack_by_name(self.zk_stack_name)
+        return resp
+
+    def after_create_zk_cluster(self) -> None:
+        self.after_create_zk_of_target_cluster()
+
+    def after_create_zk_of_target_cluster(self, cluster_num: int = None) -> None:
+        if cluster_num:
+            zk_stack = self.generate_stack_of_scaled_cluster_for_zk(cluster_num)
+        else:
+            zk_stack = self.zk_stack_name
+        zk_ips = self.get_instance_ips_of_zks_stack_by_name(zk_stack)
+        zk_ids = self.get_instance_ids_of_zks_stack_by_name(zk_stack)
+        # Check related instances status before refresh zks and start them
+        for zk_id in zk_ids:
+            assert self.is_ec2_instance_running(zk_id), f'Instance {zk_id} is not running, please start it first.'
+
+        # refresh zk cluster cfg, because the zk cfg was not included
+        # FIXME: it's hard code to make sure that zks were already initialized.
+        time.sleep(10)
+
+        self.refresh_zks_cfg(zk_ips=zk_ips, zk_ids=zk_ids)
+        self.start_zks(zk_ids=zk_ids, zk_ips=zk_ips)
+
+    def refresh_zks_cfg(self, zk_ips: List, zk_ids: List) -> None:
+        assert len(zk_ips) == 3, f'Initialized zookeeper ips num is 3, not {len(zk_ips)}.'
+        assert len(zk_ids) == 3, f'Initialized zookeeper ids num is 3, not {len(zk_ids)}.'
+        configured_zks = self.configured_zks_cfg(zk_ids=zk_ids, zk_ips=zk_ips)
+        if self.is_configured_zks(configured_zks):
+            logger.warning('Zookeepers already configured, skip configure.')
+            return
+        need_to_configure_zks = [zk_id for zk_id in zk_ids if zk_id not in configured_zks]
+        refresh_command = Commands.ZKS_CFG_COMMAND.value.format(host1=zk_ips[0], host2=zk_ips[1], host3=zk_ips[2])
+        for zk_id in need_to_configure_zks:
+            self.exec_script_instance_and_return(name_or_id=zk_id, script=refresh_command)
+
+    @staticmethod
+    def is_configured_zks(configured_zks: List) -> bool:
+        return len(configured_zks) == 3
+
+    def configured_zks_cfg(self, zk_ids: List, zk_ips: List) -> List:
+        check_command = Commands.ZKS_CHECK_CONFIGURED_COMMAND.value
+        configured_instances = []
+        for zk_id, zk_ip in zip(zk_ids, zk_ips):
+            resp = self.exec_script_instance_and_return(zk_id, check_command.format(host=zk_ip))
+            if resp['StandardOutputContent'] == '0\n':
+                logger.warning(f'Instance: {zk_id} which ip is {zk_ip} already configured zoo.cfg.')
+                configured_instances.append(zk_id)
+        return configured_instances
+
+    def start_zks(self, zk_ids: List, zk_ips: List) -> None:
+        assert len(zk_ids) == 3, f'Expected to start 3 zookeepers, not {len(zk_ids)}.'
+        started_zks = self.started_zks(zk_ids=zk_ids, zk_ips=zk_ips)
+        if self.is_started_zks(started_zks=started_zks):
+            logger.warning('Zookeepers already started.')
+            return
+
+        need_to_start_zks = [zk_id for zk_id in zk_ids if zk_id not in started_zks]
+        start_zk_command = Commands.ZKS_START_COMMAND.value
+        for zk_id in need_to_start_zks:
+            self.exec_script_instance_and_return(name_or_id=zk_id, script=start_zk_command)
+        logger.info('Start zookeepers successfully.')
+
+    @staticmethod
+    def is_started_zks(started_zks: List) -> bool:
+        return len(started_zks) == 3
+
+    def started_zks(self, zk_ids: List, zk_ips: List) -> List:
+        check_command = Commands.ZKS_CHECK_STARTED_COMMAND.value
+        started_instances = []
+        for zk_id, zk_ip in zip(zk_ids, zk_ips):
+            resp = self.exec_script_instance_and_return(zk_id, check_command)
+            if resp['StandardOutputContent'] == '0\n':
+                logger.warning(f'Instance: {zk_id} which ip is {zk_ip} already started.')
+                started_instances.append(zk_id)
+        return started_instances
+
+    def get_target_cluster_zk_host(self, cluster_num: int = None) -> str:
+        if cluster_num:
+            zk_stack_name = self.generate_stack_of_scaled_cluster_for_zk(cluster_num)
+        else:
+            zk_stack_name = self.zk_stack_name
+        return self.get_zookeepers_host_by_zk_stack_name(zk_stack_name)
+
+    def get_zookeepers_host_by_zk_stack_name(self, zk_stack_name) -> str:
+        zk_ips = self.get_instance_ips_of_zks_stack_by_name(zk_stack_name)
+        res = ','.join([zk_ip + ':2181' for zk_ip in zk_ips])
+        return res
+
+    def get_zks_basic_msg(self) -> List:
+        return self.get_zks_of_target_cluster_msg()
+
+    def get_zks_of_target_cluster_msg(self, cluster_num: int = None) -> List:
+        if cluster_num:
+            zk_stack_name = self.generate_stack_of_scaled_cluster_for_zk(cluster_num)
+        else:
+            zk_stack_name = self.zk_stack_name
+
+        if not self.is_stack_complete(zk_stack_name):
+            return []
+
+        res = [
+            Params.ZOOKEEPER_NAME_IN_CLUSTER.value.format(
+                num=i, cluster=cluster_num if cluster_num else 'default') + '\t'
+            + self.get_specify_resource_from_output(
+                zk_stack_name, Params.ZOOKEEPER_INSTANCE_ID.value.format(num=i)) + '\t'
+            + self.get_specify_resource_from_output(zk_stack_name,
+                                                    Params.ZOOKEEPER_IP.value.format(num=i)) + '\t'
+            + self.get_specify_resource_from_output(zk_stack_name, Params.ZOOKEEPER_PUB_IP.value.format(num=i))
+            for i in range(1, 4)
+        ]
+        return res
+
+    # ============ Zookeeper Services End ============
+
+    # ============ kylin Services Start ============
+    def create_kylin_stack(self) -> Optional[Dict]:
+        return self.create_kylin_stack_by_cluster()
+
+    def create_kylin_stack_by_cluster(self, cluster_num: int = None) -> Optional[Dict]:
+        if cluster_num:
+            kylin_stack_name = self.generate_stack_of_scaled_cluster_for_kylin(cluster_num)
+            zk_stack = self.generate_stack_of_scaled_cluster_for_zk(cluster_num)
+            spark_master_stack = self.generate_stack_of_scaled_cluster_for_spark_master(cluster_num)
+        else:
+            kylin_stack_name = self.kylin_stack_name
+            zk_stack = self.zk_stack_name
+            spark_master_stack = self.spark_master_stack_name
+
+        if self.is_stack_complete(kylin_stack_name):
+            return
+
+        params: Dict = self.config[Config.EC2_KYLIN4_PARAMS.value]
+        params = self.update_basic_params(params)
+        # update extra params
+        params[Params.SPARK_MASTER_HOST.value] = self.get_spark_master_host_by_name(spark_master_stack)
+        params[Params.ZOOKEEPER_HOSTS.value] = self.get_zookeepers_host_by_zk_stack_name(zk_stack)
+        params[Params.DB_HOST.value] = self.get_db_host()
+        params[Params.CLUSTER_NUM.value] = str(cluster_num) if cluster_num else 'default'
+
+        resp = self.create_stack(
+            stack_name=kylin_stack_name,
+            file_path=self.path_of_kylin_stack,
+            params=params
+        )
+        assert self.is_stack_complete(kylin_stack_name), f"Create scaled kylin stack not complete, please check."
+        return resp
+
+    def terminate_kylin_stack(self) -> Optional[Dict]:
+        resp = self.terminate_stack_by_name(self.kylin_stack_name)
+        return resp
+
+    def get_kylin_private_ip(self) -> str:
+        return self.get_kylin_private_ip_of_target_cluster()
+
+    def get_kylin_private_ip_of_target_cluster(self, cluster_num: int = None) -> str:
+        kylin_stack = self.get_kylin_stack_name(cluster_num)
+        return self._get_kylin_private_ip(kylin_stack)
+
+    def get_kylin_private_ip_by_name(self, stack_name: str) -> str:
+        return self._get_kylin_private_ip(stack_name)
+
+    def get_scaled_kylin_private_ip(self, stack_name: str) -> str:
+        return self._get_kylin_private_ip(stack_name)
+
+    def _get_kylin_private_ip(self, stack_name: str) -> str:
+        if not self.is_stack_complete(stack_name):
+            return ""
+        return self.get_specify_resource_from_output(stack_name, Params.KYLIN4_PRIVATE_IP.value)
+
+    def get_kylin_public_ip(self) -> str:
+        return self.get_kylin_public_ip_of_target_cluster()
+
+    def get_kylin_public_ip_of_target_cluster(self, cluster_num: int = None) -> str:
+        kylin_stack = self.get_kylin_stack_name(cluster_num)
+        if not self.is_stack_complete(kylin_stack):
+            return ''
+        if not self.is_associated_public_ip:
+            logger.warning('Current kylin was associated to a public ip.')
+            return ''
+        return self.get_specify_resource_from_output(kylin_stack, Params.KYLIN4_PUBLIC_IP.value)
+
+    def get_kylin_instance_id(self) -> str:
+        return self.get_kylin_instance_id_of_target_cluster()
+
+    def get_kylin_instance_id_of_target_cluster(self, cluster_num: int = None) -> str:
+        kylin_stack = self.get_kylin_stack_name(cluster_num)
+        if not self.is_stack_complete(kylin_stack):
+            return ''
+        return self.get_specify_resource_from_output(kylin_stack, Params.INSTANCE_ID.value)
+
+    def get_kylin_basic_msg(self) -> str:
+        return self.get_kylin_basic_msg_of_target_cluster()
+
+    def get_kylin_basic_msg_of_target_cluster(self, cluster_num: int = None) -> str:
+        if not self.is_stack_complete(self.static_service_stack_name):
+            return ''
+        if not self.is_stack_complete(self.get_kylin_stack_name(cluster_num=cluster_num)):
+            return ''
+        res = Params.KYLIN_NAME_IN_CLUSTER.value.format(cluster=cluster_num if cluster_num else 'default') + '\t' \
+              + self.get_kylin_instance_id_of_target_cluster(cluster_num=cluster_num) + '\t' \
+              + self.get_kylin_private_ip_of_target_cluster(cluster_num=cluster_num) + '\t' \
+              + self.get_kylin_public_ip_of_target_cluster(cluster_num=cluster_num)
+        return res
+
+    def get_kylin_stack_name(self, cluster_num: int = None) -> str:
+        if cluster_num:
+            kylin_stack = self.generate_stack_of_scaled_cluster_for_kylin(cluster_num)
+        else:
+            kylin_stack = self.kylin_stack_name
+
+        return kylin_stack
+
+    def get_scaled_kylin_private_ip(self, stack_name: str) -> str:
+        if not self.is_stack_complete(stack_name):
+            return ''
+        return self.get_specify_resource_from_output(stack_name, Params.KYLIN4_PRIVATE_IP.value)
+
+    def get_scaled_kylin_public_ip(self, stack_name: str) -> str:
+        if not self.is_stack_complete(stack_name):
+            return ""
+        if not self.is_associated_public_ip:
+            logger.warning('Current scaled kylin node was associated to a public ip.')
+            return ''
+        return self.get_specify_resource_from_output(stack_name, Params.KYLIN4_PUBLIC_IP.value)
+
+    def get_scaled_kylin_basic_msg(self) -> List:
+        return self.get_scaled_kylin_basic_msg_of_target_cluster()
+
+    def get_scaled_kylin_basic_msg_of_target_cluster(self, cluster_num: int = None) -> List:
+        msgs = []
+        for stack in self.scaled_target_kylin_stacks(cluster_num):
+            if not self.is_stack_complete(stack):
+                continue
+
+            instance_id = self.get_instance_id(stack)
+            private_ip = self.get_scaled_kylin_private_ip(stack)
+            public_ip = self.get_scaled_kylin_public_ip(stack)
+            msg = stack + '\t' + instance_id + '\t' + private_ip + '\t' + public_ip + '\t'
+            msgs.append(msg)
+        return msgs
+
+    def scale_up_kylin(self, kylin_num: int, cluster_num: int = None) -> Optional[Dict]:
+        """
+        add kylin node
+        """
+
+        if cluster_num:
+            spark_master_stack = self.generate_stack_of_scaled_cluster_for_spark_master(cluster_num=cluster_num)
+            zk_stack = self.generate_stack_of_scaled_cluster_for_zk(cluster_num=cluster_num)
+        else:
+            spark_master_stack = self.spark_master_stack_name
+            zk_stack = self.zk_stack_name
+
+        if not cluster_num:
+            cluster_num = Cluster.DEFAULT.value
+
+        stack_name = Params.KYLIN_SCALE_TARGET_CLUSTER_STACK_NAME.value.format(
+            num=kylin_num, cluster=cluster_num)
+
+        self._validate_kylin_of_target_cluster_scale(stack_name=stack_name, cluster_num=cluster_num)
+
+        if self.is_stack_complete(stack_name):
+            return
+
+        params: Dict = self.config[Config.EC2_KYLIN4_SCALE_PARAMS.value]
+        # update extra params
+        params = self.update_basic_params(params)
+        params[Params.SPARK_MASTER_HOST.value] = self.get_spark_master_host_by_name(spark_master_stack)
+        params[Params.ZOOKEEPER_HOSTS.value] = self.get_zookeepers_host_by_zk_stack_name(zk_stack)
+        params[Params.DB_HOST.value] = self.get_db_host()
+        params[Params.CLUSTER_NUM.value] = str(cluster_num)
+        params[Params.IS_SCALED.value] = 'true'
+
+        resp = self.create_stack(
+            stack_name=stack_name,
+            file_path=self.path_of_kylin_scale_stack,
+            params=params
+        )
+        assert self.is_stack_complete(stack_name)
+        return resp
+
+    def scale_down_kylin(self, kylin_num: int, cluster_num: int = None) -> Optional[Dict]:
+        if not cluster_num:
+            cluster_num = Cluster.DEFAULT.value
+        stack_name = Params.KYLIN_SCALE_TARGET_CLUSTER_STACK_NAME.value.format(num=kylin_num, cluster=cluster_num)
+        self._validate_kylin_of_target_cluster_scale(stack_name=stack_name, cluster_num=cluster_num)
+
+        # before terminate and delete stack, the worker should be decommissioned.
+        resp = self.terminate_stack_by_name(stack_name)
+
+        assert self.is_stack_deleted_complete(stack_name)
+        return resp
+
+    # ============ kylin Services End ============
+
+    # ============ Spark Master Services Start ============
+    def get_target_cluster_spark_master_host(self, cluster_num: int = None) -> str:
+        if cluster_num:
+            spark_master_stack = self.generate_stack_of_scaled_cluster_for_spark_master(cluster_num)
+        else:
+            spark_master_stack = self.spark_master_stack_name
+
+        return self.get_spark_master_host_by_name(spark_master_stack)
+
+    def get_spark_master_host(self) -> str:
+        return self.get_spark_master_host_of_target_cluster()
+
+    def get_spark_master_host_of_target_cluster(self, cluster_num: int = None) -> str:
+        spark_master_stack = self.get_spark_master_stack_name(cluster_num)
+        return self.get_spark_master_host_by_name(spark_master_stack)
+
+    def get_spark_master_host_by_name(self, spark_master_stack_name: str) -> str:
+        if not self.is_stack_complete(spark_master_stack_name):
+            return ''
+
+        return self.get_specify_resource_from_output(spark_master_stack_name, Params.SPARK_MASTER_HOST.value)
+
+    def get_spark_master_public_ip(self) -> str:
+        return self.get_spark_master_public_ip_of_target_cluster()
+
+    def get_spark_master_public_ip_of_target_cluster(self, cluster_num: int = None) -> str:
+        spark_master_stack = self.get_spark_master_stack_name(cluster_num)
+        if not self.is_stack_complete(spark_master_stack):
+            return ''
+
+        return self.get_specify_resource_from_output(spark_master_stack, Params.SPARK_PUB_IP.value)
+
+    def get_spark_master_stack_name(self, cluster_num: int = None) -> str:
+        if cluster_num:
+            spark_master_stack_name = self.generate_stack_of_scaled_cluster_for_spark_master(cluster_num)
+        else:
+            spark_master_stack_name = self.spark_master_stack_name
+
+        return spark_master_stack_name
+
+    def create_spark_master_stack(self) -> Optional[Dict]:
+        return self.create_spark_master_stack_by_cluster()
+
+    def create_spark_master_stack_by_cluster(self, cluster_num: int = None) -> Optional[Dict]:
+        if cluster_num:
+            spark_master_stack_name = self.generate_stack_of_scaled_cluster_for_spark_master(cluster_num)
+        else:
+            spark_master_stack_name = self.spark_master_stack_name
+
+        if self.is_stack_complete(spark_master_stack_name):
+            return
+
+        params: Dict = self.config[Config.EC2_SPARK_MASTER_PARAMS.value]
+        # update needed params
+        params = self.update_basic_params(params)
+        params[Params.DB_HOST.value] = self.get_db_host()
+        resp = self.create_stack(
+            stack_name=spark_master_stack_name,
+            file_path=self.path_of_spark_master_stack,
+            params=params
+        )
+
+        assert self.is_stack_complete(spark_master_stack_name), \
+            f'Current {spark_master_stack_name} stack not create complete, please check.'
+        return resp
+
+    def terminate_spark_master_stack(self) -> Optional[Dict]:
+        resp = self.terminate_stack_by_name(self.spark_master_stack_name)
+        return resp
+
+    def start_spark_master(self) -> None:
+        spark_master_id = self.get_instance_id(self.spark_master_stack_name)
+        if not self.is_ec2_instance_running(spark_master_id):
+            msg = f'Instance of spark master{spark_master_id} was not running, please start it first.'
+            logger.error(msg)
+            raise Exception(msg)
+
+        start_command = Commands.START_SPARK_MASTER_COMMAND.value
+        self.exec_script_instance_and_return(name_or_id=spark_master_id, script=start_command)
+
+    def get_spark_master_instance_id(self) -> str:
+        return self.get_target_cluster_spark_master_host()
+
+    def get_spark_master_instance_id_of_target_cluster(self, cluster_num: int = None) -> str:
+        spark_master_stack = self.get_spark_master_stack_name(cluster_num)
+
+        if not self.is_stack_complete(spark_master_stack):
+            return ''
+
+        return self.get_specify_resource_from_output(spark_master_stack, Params.INSTANCE_ID.value)
+
+    def get_spark_master_msg(self) -> str:
+        return self.get_spark_master_of_target_cluster_msg()
+
+    def get_spark_master_of_target_cluster_msg(self, cluster_num: int = None) -> str:
+        if cluster_num:
+            spark_master_stack_name = self.generate_stack_of_scaled_cluster_for_spark_master(cluster_num)
+        else:
+            spark_master_stack_name = self.spark_master_stack_name
+
+        if not self.is_stack_complete(spark_master_stack_name):
+            return ''
+
+        res = Params.SPARK_MASTER_NAME_IN_CLUSTER.value.format(
+              cluster=cluster_num if cluster_num else 'default') + '\t' \
+              + self.get_spark_master_instance_id_of_target_cluster(cluster_num) + '\t' \
+              + self.get_spark_master_host_of_target_cluster(cluster_num) + '\t' \
+              + self.get_spark_master_public_ip_of_target_cluster(cluster_num)
+        return res
+
+    # ============ Spark Master Services End ============
+
+    # ============ Spark Slave Services Start ============
+    def create_spark_slave_stack(self) -> Optional:
+        return self.create_spark_slave_stack_by_cluster()
+
+    def create_spark_slave_stack_by_cluster(self, cluster_num: int = None) -> Optional[Dict]:
+        if cluster_num:
+            spark_master_stack = self.generate_stack_of_scaled_cluster_for_spark_master(cluster_num)
+            spark_worker_stack = self.generate_stack_of_scaled_cluster_for_spark_worker(cluster_num)
+        else:
+            spark_master_stack = self.spark_master_stack_name
+            spark_worker_stack = self.spark_slave_stack_name
+
+        if not self.is_stack_complete(spark_master_stack):
+            msg = f'Spark master {spark_master_stack} must be created before create spark slaves.'
+            logger.error(msg)
+            raise Exception(msg)
+
+        if self.is_stack_complete(spark_worker_stack):
+            return
+
+        params: Dict = self.config[Config.EC2_SPARK_WORKER_PARAMS.value]
+        params = self.update_basic_params(params)
+        params[Params.SPARK_MASTER_HOST.value] = self.get_spark_master_host_by_name(spark_master_stack)
+
+        resp = self.create_stack(
+            stack_name=spark_worker_stack,
+            file_path=self.path_of_spark_slave_stack,
+            params=params
+        )
+        return resp
+
+    def terminate_spark_slave_stack(self) -> Optional[Dict]:
+        resp = self.terminate_stack_by_name(self.spark_slave_stack_name)
+        return resp
+
+    def get_instance_ids_of_spark_slave_stack(self) -> Optional[List]:
+        if not self.is_stack_complete(self.spark_slave_stack_name):
+            return
+        return [
+            self.get_specify_resource_from_output(
+                self.spark_slave_stack_name, Params.SPARK_WORKER_ID.value.format(num=i))
+            for i in range(1, 4)
+        ]
+
+    def get_instance_ips_of_spark_slaves_stack(self) -> List:
+        return self.get_instance_ips_of_spark_slaves_stack_by_name(self.spark_slave_stack_name)
+
+    def get_instance_ips_of_spark_slaves_stack_by_name(self, stack_name: str) -> List:
+        if not self.is_stack_complete(stack_name):
+            return []
+        return [
+            self.get_specify_resource_from_output(
+                stack_name, Params.SPARK_WORKER_PRIVATE_IP.value.format(num=i))
+            for i in range(1, 4)
+        ]
+
+    def get_scaled_spark_worker_private_ip(self, stack_name: str) -> str:
+        if not self.is_stack_complete(stack_name):
+            return ''
+        return self.get_specify_resource_from_output(stack_name, Params.SPARK_SCALED_WORKER_PRIVATE_IP.value)
+
+    def get_scaled_spark_worker_public_ip(self, stack_name: str) -> str:
+        if not self.is_stack_complete(stack_name):
+            return ''
+        if not self.is_associated_public_ip:
+            logger.warning('Current spark worker was associated to a public ip.')
+            return ''
+        return self.get_specify_resource_from_output(stack_name, Params.SPARK_SCALED_WORKER_PUBLIC_IP.value)
+
+    def get_spark_slaves_basic_msg(self) -> List:
+        if not self.is_stack_complete(self.spark_slave_stack_name):
+            return []
+
+        res = [
+            Params.SPARK_WORKER_NAME.value.format(num=i) + '\t'
+            + self.get_specify_resource_from_output(
+                self.spark_slave_stack_name, Params.SPARK_WORKER_ID.value.format(num=i)) + '\t'
+            + self.get_specify_resource_from_output(
+                self.spark_slave_stack_name, Params.SPARK_WORKER_PRIVATE_IP.value.format(num=i)) + '\t'
+            + (self.get_specify_resource_from_output(
+                self.spark_slave_stack_name, Params.SPARK_WORKER_PUBLIC_IP.value.format(num=i))
+               if self.is_associated_public_ip else '')
+            for i in range(1, 4)
+        ]
+        return res
+
+    def get_scaled_spark_workers_basic_msg(self) -> List:
+        return self.get_scaled_spark_workers_basic_msg_of_target_cluster()
+
+    def get_spark_workers_of_target_cluster_msg(self, cluster_num: int = None) -> str:
+        if cluster_num:
+            spark_worker_stack_name = self.generate_stack_of_scaled_cluster_for_spark_worker(cluster_num)
+        else:
+            spark_worker_stack_name = self.spark_slave_stack_name
+
+        if not self.is_stack_complete(spark_worker_stack_name):
+            return []
+
+        res = [
+            Params.SPARK_WORKER_NAME_IN_CLUSTER.value.format(num=i, cluster=cluster_num) + '\t'
+            + self.get_specify_resource_from_output(
+                spark_worker_stack_name, Params.SPARK_WORKER_ID.value.format(num=i)) + '\t'
+            + self.get_specify_resource_from_output(
+                spark_worker_stack_name, Params.SPARK_WORKER_PRIVATE_IP.value.format(num=i)) + '\t'
+            + (self.get_specify_resource_from_output(
+                spark_worker_stack_name, Params.SPARK_WORKER_PUBLIC_IP.value.format(num=i))
+               if self.is_associated_public_ip else '')
+            for i in range(1, 4)
+        ]
+        return res
+
+    def get_scaled_spark_workers_basic_msg_of_target_cluster(self, cluster_num: int = None) -> List:
+        msgs = []
+        for stack in self.scaled_target_spark_workers_stacks(cluster_num):
+            if not self.is_stack_complete(stack):
+                continue
+
+            instance_id = self.get_instance_id(stack)
+            private_ip = self.get_scaled_spark_worker_private_ip(stack)
+            public_ip = self.get_scaled_spark_worker_public_ip(stack)
+            msg = stack + '\t' + instance_id + '\t' + private_ip + '\t' + public_ip
+            msgs.append(msg)
+        return msgs
+
+    def scale_up_basic_services_for_cluster(self, cluster_num: int) -> None:
+        # create zookeeper
+        self.create_zk_stack_by_cluster(cluster_num)
+        self.after_create_zk_of_target_cluster(cluster_num)
+
+        # create spark master
+        self.create_spark_master_stack_by_cluster(cluster_num)
+
+        # create spark worker
+        self.create_spark_slave_stack_by_cluster(cluster_num)
+
+    def scale_up_kylin_for_cluster(self, cluster_num: int) -> None:
+        # create kylin
+        self.create_kylin_stack_by_cluster(cluster_num)
+
+    def destroy_cluster(self, cluster_num: int) -> None:
+        scaled_kylin_stack_name = self.generate_stack_of_scaled_cluster_for_kylin(cluster_num)
+        self.terminate_stack_by_name(scaled_kylin_stack_name)
+
+        scaled_spark_slave_stack_name = self.generate_stack_of_scaled_cluster_for_spark_worker(cluster_num)
+        self.terminate_stack_by_name(scaled_spark_slave_stack_name)
+
+        scaled_spark_master_stack_name = self.generate_stack_of_scaled_cluster_for_spark_master(cluster_num)
+        self.terminate_stack_by_name(scaled_spark_master_stack_name)
+
+        scaled_zk_stack_name = self.generate_stack_of_scaled_cluster_for_zk(cluster_num)
+        self.terminate_stack_by_name(scaled_zk_stack_name)
+
+    def generate_stack_of_scaled_cluster_for_zk(self, cluster_num: int) -> str:
+        return f'{self.zk_stack_name}-scaled-cluster-{cluster_num}'
+
+    def generate_stack_of_scaled_cluster_for_kylin(self, cluster_num: int) -> str:
+        return f'{self.kylin_stack_name}-scaled-cluster-{cluster_num}'
+
+    def generate_stack_of_scaled_cluster_for_spark_master(self, cluster_num: int) -> str:
+        return f'{self.spark_master_stack_name}-scaled-cluster-{cluster_num}'
+
+    def generate_stack_of_scaled_cluster_for_spark_worker(self, cluster_num: int) -> str:
+        return f'{self.spark_slave_stack_name}-scaled-cluster-{cluster_num}'
+
+    def scale_up_worker(self, worker_num: int, cluster_num: int = None) -> Optional[Dict]:
+        """
+        add spark workers for kylin
+        """
+
+        if cluster_num:
+            spark_master_stack = self.generate_stack_of_scaled_cluster_for_spark_master(num=cluster_num)
+        else:
+            spark_master_stack = self.spark_master_stack_name
+
+        if not cluster_num:
+            cluster_num = Cluster.DEFAULT.value
+
+        stack_name = Params.SPARK_WORKER_SCALE_TARGET_CLUSTER_STACK_NAME.value.format(
+            num=worker_num, cluster=cluster_num)
+
+        self._validate_spark_worker_of_target_cluster_scale(stack_name=stack_name, cluster_num=cluster_num)
+
+        if self.is_stack_complete(stack_name):
+            return
+
+        params: Dict = self.config[Config.EC2_SPARK_SCALE_SLAVE_PARAMS.value]
+        params = self.update_basic_params(params)
+        params[Params.SPARK_MASTER_HOST.value] = self.get_spark_master_host_by_name(spark_master_stack)
+        params[Params.SPARK_WORKER_NUM.value] = str(worker_num)
+
+        resp = self.create_stack(
+            stack_name=stack_name,
+            file_path=self.path_of_spark_slave_scaled_stack,
+            params=params
+        )
+        assert self.is_stack_complete(stack_name)
+        return resp
+
+    def scale_down_worker(self, worker_num: int, cluster_num: int = None) -> Optional[Dict]:
+        if not cluster_num:
+            cluster_num = Cluster.DEFAULT.value
+
+        stack_name = Params.SPARK_WORKER_SCALE_TARGET_CLUSTER_STACK_NAME.value.format(
+            num=worker_num, cluster=cluster_num)
+
+        self._validate_spark_worker_of_target_cluster_scale(stack_name=stack_name, cluster_num=cluster_num)
+
+        if self.is_stack_deleted_complete(stack_name):
+            return
+
+        instance_id = self.get_instance_id(stack_name)
+        # spark decommission feature start to be supported in spark 3.1.x.
+        # refer: https://issues.apache.org/jira/browse/SPARK-20624.
+        try:
+            self.exec_script_instance_and_return(
+                name_or_id=instance_id, script=Commands.SPARK_DECOMMISION_WORKER_COMMAND.value)
+            # FIXME: hard code for sleep spark worker to execute remaining jobs
+            # sleep 5 min to ensure all jobs in decommissioned workers are done
+            time.sleep(60 * 3)
+        except AssertionError as ex:
+            logger.error(ex)
+
+        # before terminate and delete stack, the worker should be decommissioned.
+        resp = self.delete_stack(stack_name)
+        assert self.is_stack_deleted_complete(stack_name)
+        return resp
+    # ============ Spark Slave Services End ============
+
+    # ============ Prometheus Services Start ============
+    def start_prometheus_first_time(self) -> None:
+        self.refresh_prometheus_param_map()
+        self.start_prometheus_server()
+
+    def start_prometheus_server(self) -> None:
+        start_command = Commands.START_PROMETHEUS_COMMAND.value
+        instance_id = self.instance_id_of_static_services
+        self.exec_script_instance_and_return(name_or_id=instance_id, script=start_command)
+
+    def stop_prometheus_server(self) -> None:
+        stop_command = Commands.STOP_PROMETHEUS_COMMAND.value
+        instance_id = self.instance_id_of_static_services
+        self.exec_script_instance_and_return(name_or_id=instance_id, script=stop_command)
+
+    def restart_prometheus_server(self) -> None:
+        if not self.is_stack_complete(self.static_service_stack_name):
+            return
+        self.stop_prometheus_server()
+        self.start_prometheus_server()
+
+    def refresh_prometheus_param_map(self) -> None:
+        static_services_id = self.instance_id_of_static_services
+        if not static_services_id:
+            raise Exception(f'Current static services stack was not create complete, please check.')
+
+        refresh_config_commands = self.refresh_prometheus_commands()
+        for command in refresh_config_commands:
+            self.exec_script_instance_and_return(name_or_id=static_services_id, script=command)
+
+        # Special support spark metrics into prometheus
+        spark_config_commands = self.refresh_spark_metrics_commands()
+        for command in spark_config_commands:
+            self.exec_script_instance_and_return(name_or_id=static_services_id, script=command)
+
+    def refresh_prometheus_commands(self) -> List:
+        params = self.prometheus_param_map()
+        # NOTE: the spaces in templates is for prometheus config's indent
+        commands = [Commands.PROMETHEUS_CFG_COMMAND.value.format(node=node, host=host) for node, host in params.items()]
+        return commands
+
+    def after_scale_prometheus_params_map_of_kylin(self, cluster_num: int = None, is_scale_down: bool = False) -> Dict:
+        kylin_ips = self.get_scaled_node_private_ips(cluster_num)[0]
+
+        if is_scale_down:
+            nodes = Utils.generate_nodes(self.scaled_down_kylin_nodes)
+        else:
+            nodes = Utils.generate_nodes(self.scaled_kylin_nodes)
+
+        kylin_node_keys = [Params.KYLIN_SCALE_NODE_NAME_IN_CLUSTER.value.format(
+            num=i, cluster=cluster_num if cluster_num else 'default')
+            for i in nodes]
+
+        kylin_params_map = dict(zip(kylin_node_keys, kylin_ips))
+        return kylin_params_map
+
+    def after_scale_prometheus_params_map_of_spark_worker(
+            self,
+            cluster_num: int = None,
+            is_scale_down: bool = False) -> Dict:
+        spark_workers_ips = self.get_scaled_node_private_ips(cluster_num)[1]
+
+        if is_scale_down:
+            nodes = Utils.generate_nodes(self.scaled_down_spark_workers)
+        else:
+            nodes = Utils.generate_nodes(self.scaled_spark_workers)
+
+        spark_workers_keys = [Params.SPARK_SCALE_WORKER_NAME_IN_CLUSTER.value.format(
+            num=i, cluster=cluster_num if cluster_num else 'default')
+            for i in nodes]
+
+        spark_workers_params_map = dict(zip(spark_workers_keys, spark_workers_ips))
+
+        return spark_workers_params_map
+
+    def after_scale_prometheus_params_map_of_cluster(self, cluster_nums: List[int]) -> Dict:
+        nodes_of_cluster_params_map = self.prometheus_param_map_of_scaled_cluster(cluster_nums)
+        return nodes_of_cluster_params_map
+
+    def after_launch_or_destroy_prometheus_params_map_of_clusters(self, cluster_nums: List[int]) -> Dict:
+        nodes_of_cluster_params_map = self.prometheus_param_map_of_launched_clusters(cluster_nums)
+        return nodes_of_cluster_params_map
+
+    def after_scale_prometheus_params_map_of_cluster_for_spark_metric(self, cluster_nums: List[int]) -> (Dict, Dict):
+        kylin_params_map = self.prometheus_spark_metric_params_of_kylin_in_cluster(cluster_nums)
+        spark_params_map = self.prometheus_spark_metric_params_of_spark_in_cluster(cluster_nums)
+        return kylin_params_map, spark_params_map
+
+    def after_launch_or_destroy_prometheus_params_of_clusters_for_spark_metric(self, cluster_nums: List[int]) -> (Dict, Dict):
+        kylin_params_map = self.prometheus_spark_metric_params_of_kylin_in_cluster(cluster_nums)
+        spark_params_map = self.prometheus_spark_metric_params_of_spark_in_cluster(cluster_nums)
+        return kylin_params_map, spark_params_map
+
+    def is_prometheus_configured(self, host: str) -> bool:
+        static_services_instance_id = self.instance_id_of_static_services
+        check_command = Commands.PROMETHEUS_CFG_CHECK_COMMAND.value.format(node=host)
+        output = self.exec_script_instance_and_return(name_or_id=static_services_instance_id, script=check_command)
+        return output['StandardOutputContent'] == '0\n'
+
+    def get_prometheus_configured_hosts(self, hosts: List) -> List:
+        static_services_instance_id = self.instance_id_of_static_services
+        configured_hosts = []
+        for host in hosts:
+            check_command = Commands.PROMETHEUS_CFG_CHECK_COMMAND.value.format(node=host)
+            output = self.exec_script_instance_and_return(name_or_id=static_services_instance_id, script=check_command)
+            if output['StandardOutputContent'] == '0\n':
+                configured_hosts.append(host)
+        return configured_hosts
+
+    def check_prometheus_config_after_scale(self, node_type: str, cluster_num: int = None) -> Dict:
+        if node_type == NodeType.KYLIN.value:
+            workers_param_map = self.after_scale_prometheus_params_map_of_kylin(cluster_num)
+        else:
+            workers_param_map = self.after_scale_prometheus_params_map_of_spark_worker(cluster_num)
+
+        return self._check_prometheus_not_exists_nodes(workers_param_map)
+
+    def check_prometheus_config_after_scale_cluster(self, cluster_nums: List[int]) -> Dict:
+        params_map = self.after_scale_prometheus_params_map_of_cluster(cluster_nums)
+        return self._check_prometheus_not_exists_nodes(params_map)
+
+    def check_prometheus_config_after_launch_clusters(self, cluster_nums: List[int]) -> Dict:
+        params_map = self.after_launch_or_destroy_prometheus_params_map_of_clusters(cluster_nums=cluster_nums)
+        return self._check_prometheus_not_exists_nodes(params_map)
+
+    def check_spark_metric_config_after_scale_cluster(self, cluster_nums: List[int]) -> (Dict, Dict):
+        kylin_params, spark_params = self.after_scale_prometheus_params_map_of_cluster_for_spark_metric(cluster_nums)
+        not_exists_kylin_nodes: Dict = self._check_prometheus_not_exists_nodes(kylin_params)
+        not_exists_spark_nodes: Dict = self._check_prometheus_not_exists_nodes(spark_params)
+        return not_exists_kylin_nodes, not_exists_spark_nodes
+
+    def check_spark_metric_config_after_launch_clusters(self, cluster_nums: List[int]) -> (Dict, Dict):
+        kylin_params, spark_params = self.after_launch_or_destroy_prometheus_params_of_clusters_for_spark_metric(
+            cluster_nums)
+        not_exists_kylin_nodes: Dict = self._check_prometheus_not_exists_nodes(kylin_params)
+        not_exists_spark_nodes: Dict = self._check_prometheus_not_exists_nodes(spark_params)
+        return not_exists_kylin_nodes, not_exists_spark_nodes
+
+    def check_spark_metric_config_after_scale_down_cluster(self, cluster_nums: List[int]) -> (Dict, Dict):
+        kylin_params, spark_params = self.after_scale_prometheus_params_map_of_cluster_for_spark_metric(cluster_nums)
+        exists_kylin_nodes: Dict = self._check_prometheus_exists_nodes(kylin_params)
+        exists_spark_nodes: Dict = self._check_prometheus_exists_nodes(spark_params)
+        return exists_kylin_nodes, exists_spark_nodes
+
+    def check_spark_metric_config_after_destroy_clusters(self, cluster_nums: List[int]) -> (Dict, Dict):
+        kylin_params, spark_params = self.after_launch_or_destroy_prometheus_params_of_clusters_for_spark_metric(
+            cluster_nums)
+        exists_kylin_nodes: Dict = self._check_prometheus_exists_nodes(kylin_params)
+        exists_spark_nodes: Dict = self._check_prometheus_exists_nodes(spark_params)
+        return exists_kylin_nodes, exists_spark_nodes
+
+    def check_prometheus_config_after_scale_down(self, node_type: str, cluster_num: int = None) -> Dict:
+        if node_type == NodeType.KYLIN.value:
+            workers_param_map = self.after_scale_prometheus_params_map_of_kylin(cluster_num, is_scale_down=True)
+        else:
+            workers_param_map = self.after_scale_prometheus_params_map_of_spark_worker(cluster_num, is_scale_down=True)
+
+        return self._check_prometheus_exists_nodes(params=workers_param_map)
+
+    def _check_prometheus_exists_nodes(self, params: Dict) -> Dict:
+        exists_nodes: Dict = {}
+        for k, v in params.items():
+            command = Commands.PROMETHEUS_CFG_CHECK_COMMAND.value.format(node=k)
+            output = self.exec_script_instance_and_return(
+                name_or_id=self.instance_id_of_static_services, script=command)
+            # output['StandardOutputContent'] = '0\n' means the node exists in the prometheus config
+            if output['StandardOutputContent'] == '0\n':
+                exists_nodes.update({k: v})
+        return exists_nodes
+
+    def _check_prometheus_not_exists_nodes(self, params: Dict) -> Dict:
+        not_exists_nodes: Dict = {}
+        for k, v in params.items():
+            command = Commands.PROMETHEUS_CFG_CHECK_COMMAND.value.format(node=k)
+            output = self.exec_script_instance_and_return(
+                name_or_id=self.instance_id_of_static_services, script=command)
+            # output['StandardOutputContent'] = '1\n' means the node not exists in the prometheus config
+            if output['StandardOutputContent'] == '1\n':
+                not_exists_nodes.update({k: v})
+        return not_exists_nodes
+
+    def check_prometheus_config_after_scale_down_cluster(self, cluster_nums: List[int]) -> Dict:
+        params_map = self.after_scale_prometheus_params_map_of_cluster(cluster_nums)
+        return self._check_prometheus_exists_nodes(params_map)
+
+    def check_prometheus_config_after_destroy_clusters(self, cluster_nums: List[int]) -> Dict:
+        params_map = self.after_launch_or_destroy_prometheus_params_map_of_clusters(cluster_nums)
+        return self._check_prometheus_exists_nodes(params_map)
+
+    def refresh_prometheus_config_after_launch_clusters(self, expected_nodes: Dict) -> None:
+        self.refresh_prometheus_config_after_scale_up(expected_nodes=expected_nodes)
+
+    def refresh_prometheus_config_after_scale_up(self, expected_nodes: Dict) -> None:
+        commands = self.refresh_prometheus_commands_after_scale(expected_nodes)
+        for command in commands:
+            self.exec_script_instance_and_return(name_or_id=self.instance_id_of_static_services, script=command)
+
+    def refresh_prometheus_spark_driver_of_kylin_after_scale_up(self, expected_nodes: Dict) -> None:
+        commands = self.refresh_prometheus_spark_driver_of_kylin_after_scale(expected_nodes)
+        for command in commands:
+            self.exec_script_instance_and_return(name_or_id=self.instance_id_of_static_services, script=command)
+
+    def refresh_prometheus_spark_metrics_of_kylin_in_cluster_after_scale_up(self, expected_nodes: Dict) -> None:
+        commands = self.refresh_prometheus_spark_driver_of_kylin_in_cluster_after_scale(expected_nodes)
+        for command in commands:
+            self.exec_script_instance_and_return(name_or_id=self.instance_id_of_static_services, script=command)
+
+    def refresh_prometheus_config_after_destroy_clusters(self, exists_nodes: Dict) -> None:
+        self.refresh_prometheus_config_after_scale_down(exists_nodes)
+
+    def refresh_prometheus_config_after_scale_down(self, exists_nodes: Dict) -> None:
+        commands = [Commands.PROMETHEUS_DELETE_CFG_COMMAND.value.format(node=worker) for worker in exists_nodes.keys()]
+        for command in commands:
+            self.exec_script_instance_and_return(name_or_id=self.instance_id_of_static_services, script=command)
+
+    def refresh_prometheus_spark_driver_of_kylin_after_scale_down(self, exists_nodes: Dict) -> None:
+        commands = [Commands.SPARK_DRIVER_METRIC_OF_KYLIN_DELETE_CFG_COMMAND.value.format(node=worker)
+                    for worker in exists_nodes.keys()]
+        for command in commands:
+            self.exec_script_instance_and_return(name_or_id=self.instance_id_of_static_services, script=command)
+
+    # ============ Prometheus Services End ============
+
+    # ============ Utils Services Start ============
+    def is_scaled_stacks_in_clusters_terminated(self, cluster_nums: List) -> bool:
+        for num in cluster_nums:
+            for stack in self.scaled_target_spark_workers_stacks():
+                if not self.is_stack_deleted_complete(stack):
+                    logger.warning(f'Scaled Spark Workers Stack:{stack} '
+                                   f'in Cluster {num} was not terminated, please check.')
+                    return False
+            for stack in self.scaled_target_kylin_stacks(num):
+                if not self.is_stack_deleted_complete(stack):
+                    logger.warning(f'Scaled Kylin Stack:{stack} '
+                                   f'in Cluster {num} was not terminated, please check.')
+                    return False
+        return True
+
+    def is_target_clusters_terminated(self, cluster_nums: List) -> bool:
+        for num in cluster_nums:
+            return self.is_target_cluster_terminated(num)
+
+    def is_target_cluster_terminated(self, cluster_num: int) -> bool:
+        zk_stack = self.generate_stack_of_scaled_cluster_for_zk(cluster_num)
+
+        if not self.is_stack_deleted_complete(zk_stack):
+            logger.warning(f'Stack {zk_stack} in cluster {cluster_num} was not terminated, please check.')
+            return False
+
+        kylin_stack = self.generate_stack_of_scaled_cluster_for_kylin(cluster_num)
+        if not self.is_stack_deleted_complete(kylin_stack):
+            logger.warning(f'Stack {kylin_stack} in cluster {cluster_num} was not terminated, please check.')
+            return False
+
+        spark_master_stack = self.generate_stack_of_scaled_cluster_for_spark_master(cluster_num)
+        if not self.is_stack_deleted_complete(spark_master_stack):
+            logger.warning(f'Stack {spark_master_stack} in cluster {cluster_num} was not terminated, please check.')
+            return False
+
+        spark_workers_stack = self.generate_stack_of_scaled_cluster_for_spark_worker(cluster_num)
+        if not self.is_stack_deleted_complete(spark_workers_stack):
+            logger.warning(f'Stack {spark_workers_stack} in cluster {cluster_num} was not terminated, please check.')
+            return False
+
+        return True
+
+
+    def update_basic_params(self, params: Dict) -> Dict:
+        params[Params.SUBNET_ID.value] = self.get_subnet_id()
+        params[Params.SECURITY_GROUP.value] = self.get_security_group_id()
+        params[Params.INSTANCE_PROFILE.value] = self.get_instance_profile()
+        # update bucket path
+        params[Params.BUCKET_FULL_PATH.value] = self.bucket_full_path
+        params[Params.BUCKET_PATH.value] = self.bucket_path
+        return params
+
+    def terminate_stack_by_name(self, stack_name: str) -> Optional[Dict]:
+        if self.is_stack_deleted_complete(stack_name):
+            return
+
+        resp = self.delete_stack(stack_name)
+        assert self.is_stack_deleted_complete(stack_name), f'Current delete stack {stack_name} failed, please check.'
+        return resp
+
+    def create_stack(self, stack_name: str, file_path: str, params: Dict, is_capability: bool = False) -> Dict:
+        try:
+            if is_capability:
+                resp = self.cf_client.create_stack(
+                    StackName=stack_name,
+                    TemplateBody=Utils.read_template(file_path),
+                    Parameters=[{'ParameterKey': k, 'ParameterValue': v} for k, v in params.items()],
+                    Capabilities=['CAPABILITY_IAM']
+                )
+            else:
+                resp = self.cf_client.create_stack(
+                    StackName=stack_name,
+                    TemplateBody=Utils.read_template(file_path),
+                    Parameters=[{'ParameterKey': k, 'ParameterValue': v} for k, v in params.items()],
+                )
+            return resp
+        except ParamValidationError as ex:
+            logger.error(ex)
+        assert self.is_stack_complete(stack_name=stack_name), \
+            f"Stack {stack_name} not create complete, please check."
+
+    def delete_stack(self, stack_name: str) -> Dict:
+        logger.info(f'Current terminating stack: {stack_name}.')
+        resp = self.cf_client.delete_stack(StackName=stack_name)
+        return resp
+
+    def refresh_spark_metrics_commands(self) -> List:
+        spark_master_host = self.get_spark_master_host()
+        # kylin ip will be the host of spark driver and spark worker
+        kylin_ip = self.get_kylin_private_ip()
+        commands = [
+            Commands.SPARK_DRIVER_METRIC_COMMAND.value.format(node='spark_driver_in_kylin', host=kylin_ip),
+            Commands.SPARK_WORKER_METRIC_COMMAND.value.format(node='spark_worker_in_kylin', host=kylin_ip),
+            Commands.SPARK_APPLICATIONS_METRIC_COMMAND.value.format(node='spark_applications', host=spark_master_host),
+            Commands.SPARK_MASTER_METRIC_COMMAND.value.format(node='spark_master', host=spark_master_host),
+            Commands.SPARK_EXECUTORS_METRIC_COMMAND.value.format(node='spark_executors', host=spark_master_host),
+        ]
+        return commands
+
+    @staticmethod
+    def refresh_prometheus_commands_after_scale(expected_nodes: Dict) -> List:
+        commands = [Commands.PROMETHEUS_CFG_COMMAND.value.format(node=node, host=host)
+                    for node, host in expected_nodes.items()]
+        return commands
+
+    @staticmethod
+    def refresh_prometheus_spark_driver_of_kylin_after_scale(expected_nodes: Dict) -> List:
+        commands = [Commands.SPARK_DRIVER_METRIC_OF_KYLIN_COMMAND.value.format(node=node, host=host)
+                    for node, host in expected_nodes.items()]
+        return commands
+
+    @staticmethod
+    def refresh_prometheus_spark_driver_of_kylin_in_cluster_after_scale(expected_nodes: Dict) -> List:
+        commands = [Commands.SPARK_DRIVER_METRIC_COMMAND.value.format(node=node, host=host)
+                    for node, host in expected_nodes.items()]
+        return commands
+
+    @staticmethod
+    def refresh_prometheus_spark_executor_in_cluster_after_scale(expected_nodes: Dict) -> List:
+        commands = [Commands.SPARK_EXECUTORS_METRIC_COMMAND.value.format(node=node, host=host)
+                    for node, host in expected_nodes.items()]
+        return commands
+
+    @staticmethod
+    def refresh_prometheus_spark_master_in_cluster_after_scale(expected_nodes: Dict) -> List:
+        commands = [Commands.SPARK_MASTER_METRIC_COMMAND.value.format(node=node, host=host)
+                    for node, host in expected_nodes.items()]
+        return commands
+
+    @staticmethod
+    def refresh_prometheus_spark_application_in_cluster_after_scale(expected_nodes: Dict) -> List:
+        commands = [Commands.SPARK_APPLICATIONS_METRIC_COMMAND.value.format(node=node, host=host)
+                    for node, host in expected_nodes.items()]
+        return commands
+
+    def prometheus_param_map(self) -> Dict:
+        params_map: Dict = {}
+        static_ip = self.get_static_services_private_ip()
+        static_map = {Params.STATIC_SERVICES_NAME.value: static_ip}
+
+        kylin_ip = self.get_kylin_private_ip()
+        kylin_map = {Params.KYLIN_NAME.value: kylin_ip}
+
+        spark_master_ip = self.get_spark_master_host()
+        spark_master_map = {Params.SPARK_MASTER_NAME.value: spark_master_ip}
+
+        zk_ips = self.get_instance_ips_of_zks_stack()
+        zks_map = {
+            Params.ZOOKEEPER_NAME.value.format(num=i): zk_ips[i - 1] for i in range(1, 4)
+        }
+        spark_slaves_ips = self.get_instance_ips_of_spark_slaves_stack()
+        spark_slaves_map = {
+            Params.SPARK_WORKER_NAME.value.format(num=i): spark_slaves_ips[i - 1] for i in range(1, 4)
+        }
+        params_map.update(static_map)
+        params_map.update(kylin_map)
+        params_map.update(spark_master_map)
+        params_map.update(zks_map)
+        params_map.update(spark_slaves_map)
+
+        return params_map
+
+    def prometheus_param_map_of_scaled_cluster(self, cluster_nums: List[int]) -> Dict:
+        params_map: Dict = {}
+        for num in cluster_nums:
+            # kylin stack
+            kylin_map = self.kylin_param_map_by_cluster_num(num)
+            if kylin_map:
+                params_map.update(kylin_map)
+
+            # spark master
+            spark_master_map = self.spark_master_param_map_by_num(num)
+            if spark_master_map:
+                params_map.update(spark_master_map)
+
+            # zookeeper
+            zks_map = self.zks_param_map_by_num(num)
+            if zks_map:
+                params_map.update(zks_map)
+
+            # spark worker
+            spark_slaves_map = self.spark_worker_param_map(num)
+            if not spark_slaves_map:
+                params_map.update(spark_slaves_map)
+
+        return params_map
+
+    def prometheus_param_map_of_launched_clusters(self, cluster_nums: List[int]) -> Dict:
+        params_map: Dict = {}
+        for num in cluster_nums:
+            param: Dict = self.prometheus_param_map_of_launched_cluster(cluster_num=num)
+            params_map.update(param)
+
+        return params_map
+
+    def prometheus_param_map_of_launched_cluster(self, cluster_num: int) -> Dict:
+        params_map: Dict = {}
+        # kylin stack
+        kylin_map = self.kylin_param_map_by_cluster_num(num=cluster_num)
+        if kylin_map:
+            params_map.update(kylin_map)
+
+        # spark master
+        spark_master_map = self.spark_master_param_map_by_num(num=cluster_num)
+        if spark_master_map:
+            params_map.update(spark_master_map)
+
+        # zookeeper
+        zks_map = self.zks_param_map_by_num(num=cluster_num)
+        if zks_map:
+            params_map.update(zks_map)
+
+        # spark worker
+        spark_slaves_map = self.spark_worker_param_map(num=cluster_num)
+        if not spark_slaves_map:
+            params_map.update(spark_slaves_map)
+
+        return params_map
+
+    def prometheus_spark_metric_params_of_kylin_in_cluster(self, cluster_nums: List[int]) -> Dict:
+        params_map: Dict = {}
+        for num in cluster_nums:
+            # kylin stack
+            kylin_map = self.spark_metric_of_kylin_param_map_by_cluster_num(num)
+            if kylin_map:
+                params_map.update(kylin_map)
+        return params_map
+
+    def prometheus_spark_metric_params_of_spark_in_cluster(self, cluster_nums: List[int]) -> Dict:
+        params_map: Dict = {}
+        for num in cluster_nums:
+            # spark master
+            spark_master_map = self.metric_of_spark_master_param_map_by_num(num)
+            if spark_master_map:
+                params_map.update(spark_master_map)
+        return params_map
+
+    def kylin_param_map_by_cluster_num(self, num: int) -> Dict:
+        # kylin stack
+        kylin_stack = self.generate_stack_of_scaled_cluster_for_kylin(num)
+        kylin_ip = self.get_kylin_private_ip_by_name(kylin_stack)
+        if not kylin_ip:
+            return {}
+        kylin_map = {Params.KYLIN_NAME_IN_CLUSTER.value.format(cluster=num): kylin_ip}
+        return kylin_map
+
+    def spark_metric_of_kylin_param_map_by_cluster_num(self, num: int) -> Dict:
+        # kylin stack
+        kylin_stack = self.generate_stack_of_scaled_cluster_for_kylin(num)
+        kylin_ip = self.get_kylin_private_ip_by_name(kylin_stack)
+        if not kylin_ip:
+            return {}
+        kylin_map = {Params.KYLIN_SPARK_DIRVER_IN_CLUSTER.value.format(cluster=num): kylin_ip}
+        return kylin_map
+
+    def spark_master_param_map_by_num(self, num: int) -> Dict:
+        # spark master
+        spark_master_stack = self.generate_stack_of_scaled_cluster_for_spark_master(num)
+        spark_master_ip = self.get_spark_master_host_by_name(spark_master_stack)
+        if not spark_master_ip:
+            return {}
+        spark_master_map = {Params.SPARK_MASTER_NAME_IN_CLUSTER.value.format(cluster=num): spark_master_ip}
+        return spark_master_map
+
+    def metric_of_spark_master_param_map_by_num(self, num: int) -> Dict:
+        # spark master
+        spark_master_stack = self.generate_stack_of_scaled_cluster_for_spark_master(num)
+        spark_master_ip = self.get_spark_master_host_by_name(spark_master_stack)
+        if not spark_master_ip:
+            return {}
+        spark_master_map = {Params.SPARK_MASTER_METRIC_IN_CLUSTER.value.format(cluster=num): spark_master_ip}
+        return spark_master_map
+
+    def zks_param_map_by_num(self, num: int) -> Dict:
+        # zookeeper
+        zk_stack = self.generate_stack_of_scaled_cluster_for_zk(num)
+        zk_ips = self.get_instance_ips_of_zks_stack_by_name(zk_stack)
+        if not zk_ips:
+            return {}
+        zks_map = {
+            Params.ZOOKEEPER_NAME_IN_CLUSTER.value.format(num=i, cluster=num): zk_ips[i - 1] for i in range(1, 4)
+        }
+        return zks_map
+
+    def spark_worker_param_map(self, num: int) -> Dict:
+        # spark worker
+        spark_worker = self.generate_stack_of_scaled_cluster_for_spark_worker(num)
+        spark_slaves_ips = self.get_instance_ips_of_spark_slaves_stack_by_name(spark_worker)
+        if not spark_slaves_ips:
+            return {}
+        spark_slaves_map = {
+            Params.SPARK_WORKER_NAME_IN_CLUSTER.value.format(num=i, cluster=num): spark_slaves_ips[i - 1]
+            for i in range(1, 4)
+        }
+        return spark_slaves_map
+
+    def zk_param_map(self) -> Dict:
+        return dict(zip(self.get_instance_ids_of_zks_stack(), self.get_instance_ips_of_zks_stack()))
+
+    def static_services_param_map(self) -> Dict:
+        return dict(zip(self.instance_id_of_static_services, self.get_static_services_private_ip()))
+
+    def spark_master_param_map(self) -> Dict:
+        return dict(zip(self.get_spark_master_instance_id(), self.get_spark_master_host()))
+
+    def spark_slaves_param_map(self) -> Dict:
+        return dict(zip(self.get_instance_ids_of_spark_slave_stack(), self.get_instance_ips_of_spark_slaves_stack()))
+
+    def kylin_param_map(self) -> Dict:
+        return dict(zip(self.get_kylin_instance_id(), self.get_kylin_private_ip()))
+
+    def get_scaled_node_private_ips(self, cluster_num: int = None) -> [List, List]:
+        if cluster_num:
+            scaled_kylin_stacks = self.scaled_target_kylin_stacks(cluster_num)
+            scaled_spark_worker_stacks = self.scaled_target_spark_workers_stacks(cluster_num)
+        else:
+            # Always get the full scale nodes in the `UP` range
+            scaled_kylin_stacks = self.scaled_kylin_stacks
+            scaled_spark_worker_stacks = self.scaled_spark_workers_stacks
+
+        scaled_kylin_ips = []
+        for stack in scaled_kylin_stacks:
+            ip = self.get_scaled_kylin_private_ip(stack)
+            if not ip:
+                continue
+            scaled_kylin_ips.append(ip)
+
+        scaled_workers_ips = []
+        for stack in scaled_spark_worker_stacks:
+            ip = self.get_scaled_spark_worker_private_ip(stack)
+            if not ip:
+                continue
+            scaled_workers_ips.append(ip)
+        return scaled_kylin_ips, scaled_workers_ips
+
+    def get_specify_resource_from_output(self, stack_name: str, resource_type: str) -> str:
+        output = self.get_stack_output(stack_name)
+        return output[resource_type]
+
+    def get_stack_output(self, stack_name: str) -> Dict:
+        is_complete = self._stack_complete(stack_name)
+        if not is_complete:
+            raise Exception(f"{stack_name} is not complete, please check.")
+        output = self.cf_client.describe_stacks(StackName=stack_name)
+        """current output format:
+        {
+            'Stacks' : [{
+                            ...,
+                            Outputs: [
+                                        {
+                                            'OutputKey': 'xxx',
+                                            'OutputValue': 'xxx',
+                                            'Description': ...    
+                                        }, 
+                                        ...]
+                        }],
+            'ResponseMEtadata': {...}
+        }
+        """
+        handled_outputs = {entry['OutputKey']: entry['OutputValue']
+                           for entry in list(output['Stacks'][0]['Outputs'])}
+
+        return handled_outputs
+
+    def alive_nodes(self, cluster_nums: List = None) -> None:
+        # default cluster
+        static_msg = self.get_static_services_basic_msg()
+        kylin_msg = self.get_kylin_basic_msg()
+        spark_master_msg = self.get_spark_master_msg()
+
+        msgs = [m for m in [static_msg, kylin_msg, spark_master_msg] if m]
+
+        spark_slaves_msg = self.get_spark_slaves_basic_msg()
+        zks_msg = self.get_zks_basic_msg()
+
+        scaled_kylins_msg = self.get_scaled_kylin_basic_msg()
+        scaled_spark_workers_msg = self.get_scaled_spark_workers_basic_msg()
+
+        msgs.extend(zks_msg)
+        msgs.extend(spark_slaves_msg)
+        msgs.extend(scaled_kylins_msg)
+        msgs.extend(scaled_spark_workers_msg)
+
+        # scaled clusters nodes
+        if cluster_nums is None:
+            cluster_nums = []
+        for num in cluster_nums:
+            zks_msg_of_target_cluster = self.get_zks_of_target_cluster_msg(num)
+            msgs.extend(zks_msg_of_target_cluster)
+
+            spark_master_msg_of_target_cluster = self.get_spark_master_of_target_cluster_msg(num)
+            kylin_msg_of_target_cluster = self.get_kylin_basic_msg_of_target_cluster(num)
+
+            for msg in [spark_master_msg_of_target_cluster, kylin_msg_of_target_cluster]:
+                if not msg:
+                    continue
+                msgs.append(msg)
+
+            spark_workers_msgs_of_target_cluster = self.get_spark_workers_of_target_cluster_msg(num)
+            msgs.extend(spark_workers_msgs_of_target_cluster)
+
+            scaled_kylins_msg_of_target_cluster = self.get_scaled_kylin_basic_msg_of_target_cluster(num)
+            scaled_workers_msg_of_target_cluster = self.get_scaled_spark_workers_basic_msg_of_target_cluster(num)
+            msgs.extend(scaled_kylins_msg_of_target_cluster)
+            msgs.extend(scaled_workers_msg_of_target_cluster)
+
+        header_msg = '\n=================== List Alive Nodes ===========================\n'
+        result = header_msg + f"Stack Name\t\tInstance ID\t\tPrivate Ip\t\tPublic Ip\t\t\n"
+        for msg in msgs:
+            result += msg + '\n'
+        result += header_msg
+        logger.info(result)
+
+    def is_ec2_stacks_ready(self) -> bool:
+        if not (
+                self.is_stack_complete(self.vpc_stack_name)
+                and self.is_stack_complete(self.rds_stack_name)
+                and self.is_stack_complete(self.static_service_stack_name)
+                and self.is_stack_complete(self.zk_stack_name)
+                and self.is_stack_complete(self.spark_master_stack_name)
+                and self.is_stack_complete(self.spark_slave_stack_name)
+                and self.is_stack_complete(self.kylin_stack_name)
+        ):
+            return False
+        return True
+
+    def is_target_ec2_stacks_ready(self, cluster_num: int) -> bool:
+        # Note:
+        #  target cluster already must contains zookeeper stack,
+        #  spark master stack, spark worker stack and kylin_stack
+        zk_stack = self.generate_stack_of_scaled_cluster_for_zk(cluster_num)
+        kylin_stack = self.generate_stack_of_scaled_cluster_for_kylin(cluster_num)
+        spark_master_stack = self.generate_stack_of_scaled_cluster_for_spark_master(cluster_num)
+        spark_worker_stack = self.generate_stack_of_scaled_cluster_for_spark_worker(cluster_num)
+        if not (
+                self.is_stack_complete(zk_stack)
+                and self.is_stack_complete(kylin_stack)
+                and self.is_stack_complete(spark_master_stack)
+                and self.is_stack_complete(spark_worker_stack)
+        ):
+            return False
+
+        return True
+
+    def is_prepared_for_scale_cluster(self) -> bool:
+        if not (
+                self.is_stack_complete(self.vpc_stack_name)
+                and self.is_stack_complete(self.rds_stack_name)
+                and self.is_stack_complete(self.static_service_stack_name)
+        ):
+            return False
+        return True
+
+    def is_ec2_stacks_terminated(self) -> bool:
+        deleted_cost_stacks: bool = (
+                self.is_stack_deleted_complete(self.static_service_stack_name)
+                and self.is_stack_deleted_complete(self.zk_stack_name)
+                and self.is_stack_deleted_complete(self.spark_master_stack_name)
+                and self.is_stack_deleted_complete(self.spark_slave_stack_name)
+                and self.is_stack_deleted_complete(self.kylin_stack_name)
+        )
+        if not deleted_cost_stacks:
+            return False
+        if not self.config['ALWAYS_DESTROY_ALL'] \
+                or self.is_stack_deleted_complete(self.vpc_stack_name):
+            return True
+        return False
+
+    def send_command(self, **kwargs) -> Dict:
+        instance_ids = kwargs['vm_name']
+        script = kwargs['script']
+        document_name = "AWS-RunShellScript"
+        parameters = {'commands': [script]}
+        response = self.ssm_client.send_command(
+            InstanceIds=instance_ids,
+            DocumentName=document_name,
+            Parameters=parameters
+        )
+        return response
+
+    def get_command_invocation(self, command_id, instance_id) -> Dict:
+        response = self.ssm_client.get_command_invocation(CommandId=command_id, InstanceId=instance_id)
+        return response
+
+    def exec_script_instance_and_return(self, name_or_id: str, script: str, timeout: int = 20) -> Dict:
+        vm_name = None
+        if isinstance(name_or_id, str):
+            vm_name = [name_or_id]
+        response = self.send_command(vm_name=vm_name, script=script)
+        command_id = response['Command']['CommandId']
+        time.sleep(5)
+        start = time.time()
+        output = None
+        while time.time() - start < timeout * 60:
+            output = self.get_command_invocation(
+                command_id=command_id,
+                instance_id=name_or_id,
+            )
+            if output['Status'] in ['Delayed', 'Success', 'Cancelled', 'TimedOut', 'Failed']:
+                break
+            time.sleep(10)
+        assert output and output['Status'] == 'Success', \
+            f"execute script failed, failed info: {output['StandardErrorContent']}"
+        return output
+
+    def stop_ec2_instance(self, instance_id: str):
+        self.ec2_client.stop_instances(
+            InstanceIds=[
+                instance_id,
+            ],
+            Force=True
+        )
+
+    def stop_ec2_instances(self, instance_ids: List):
+        self.ec2_client.stop_instances(
+            InstanceIds=instance_ids,
+            Force=True
+        )
+
+    def start_ec2_instance(self, instance_id: str) -> Dict:
+        resp = self.ec2_client.start_instances(
+            InstanceIds=[instance_id]
+        )
+        return resp
+
+    def start_ec2_instances(self, instance_ids: List) -> Dict:
+        resp = self.ec2_client.start_instances(
+            InstanceIds=instance_ids,
+        )
+        return resp
+
+    def ec2_instance_statuses(self, instance_ids: List) -> Dict:
+        resp = self.ec2_client.describe_instance_status(
+            Filters=[{
+                'Name': 'instance-state-name',
+                'Values': ['pending', 'running', 'shutting-down', 'terminated', 'stopping', 'stopped'],
+            }],
+            InstanceIds=instance_ids,
+            # Note: IncludeAllInstances (boolean), Default is false.
+            # When `true` , includes the health status for all instances.
+            # When `false` , includes the health status for running instances only.
+            IncludeAllInstances=True,
+        )
+        return resp['InstanceStatuses']
+
+    def ec2_instance_status(self, instance_id: str) -> str:
+        resp = self.ec2_instance_statuses(instance_ids=[instance_id])
+        assert resp, 'Instance statuses must be not empty.'
+        return resp[0]['InstanceState']['Name']
+
+    def is_ec2_instance_running(self, instance_id: str) -> bool:
+        return self.ec2_instance_status(instance_id) == 'running'
+
+    def is_ec2_instance_stopped(self, instance_id: str) -> bool:
+        return self.ec2_instance_status(instance_id) == 'stopped'
+
+    def is_rds_ready(self) -> bool:
+        describe_rds: Dict = self.get_rds_describe()
+        if not describe_rds:
+            return False
+        rds_endpoints: Dict = describe_rds['Endpoint']
+        # TODO: check rds with password and user is accessible.
+
+        is_rds_available: bool = describe_rds['DBInstanceStatus'] == 'available'
+        is_rds_matched_port = str(rds_endpoints['Port']) == self.db_port
+        return is_rds_available and is_rds_matched_port
+
+    def valid_s3_bucket(self) -> None:
+        if not self.is_s3_directory_exists(self.bucket, self.bucket_dir):
+            msg = f'Invalid S3 bucket path: {self.bucket_full_path}, please check.'
+            raise Exception(msg)
+
+    def valid_iam_role(self) -> None:
+        if not self.is_iam_role_exists():
+            msg = f'Invalid IAM role: {self.iam_role}, please check.'
+            raise Exception(msg)
+
+    def is_iam_role_exists(self) -> bool:
+        try:
+            self.iam_client.get_role(RoleName=self.iam_role)
+            return True
+        except self.iam_client.exceptions.NoSuchEntityException:
+            return False
+
+    def valid_key_pair(self) -> None:
+        if not self.is_key_pair_exists():
+            msg = f'Invalid KeyPair: {self.key_pair}. please check.'
+            raise Exception(msg)
+
+    def valid_cidr_ip(self) -> None:
+        if not self.is_valid_cidr_ip():
+            msg = f'Invalid CidrIp: {self.cidr_ip}. please check.'
+            raise Exception(msg)
+
+    def is_key_pair_exists(self) -> bool:
+        try:
+            self.ec2_client.describe_key_pairs(KeyNames=[self.key_pair])
+            return True
+        except ClientError:
+            return False
+
+    def is_valid_cidr_ip(self) -> bool:
+        if not self.cidr_ip:
+            return False
+
+        pattern = r'^([0-9]{1,3}\.){3}[0-9]{1,3}($|/(16|24|32))$'
+        res = re.match(pattern=pattern, string=self.cidr_ip)
+        if not res:
+            logger.error(f'Current {self.cidr_ip} not match correct pattern of CidrIp, please check.')
+            return False
+        return True
+
+    def is_object_exists_on_s3(self, filename: str, bucket: str, bucket_dir: str) -> bool:
+        try:
+            self.s3_client.head_object(Bucket=bucket, Key=bucket_dir + filename)
+        except botocore.exceptions.ClientError as ex:
+            assert ex.response['Error']['Code'] == '404'
+            return False
+        return True
+
+    def is_s3_directory_exists(self, bucket: str, bucket_dir: str) -> bool:
+        if bucket_dir.endswith('/'):
+            bucket_dir = bucket_dir.rstrip('/')
+
+        resp = self.s3_client.list_objects(Bucket=bucket, Prefix=bucket_dir, Delimiter='/', MaxKeys=1)
+        return 'CommonPrefixes' in resp
+
+    def upload_tars_to_s3(self, tars: List) -> None:
+        for tar in tars:
+            if self.is_object_exists_on_s3(tar, self.bucket, self.bucket_tars_dir):
+                logger.info(f'{tar} already exists, skip upload it.')
+                continue
+            self.upload_file_to_s3(TARS_PATH, tar, self.bucket, self.bucket_tars_dir)
+
+    def check_tars_on_s3(self, tars: List) -> None:
+        for tar in tars:
+            assert self.is_object_exists_on_s3(tar, self.bucket, self.bucket_tars_dir), \
+                f'{tar} not exists, please check.'
+
+    def upload_jars_to_s3(self, jars: List) -> None:
+        for jar in jars:
+            if self.is_object_exists_on_s3(jar, self.bucket, self.bucket_jars_dir):
+                logger.info(f'{jar} already exists, skip upload it.')
+                continue
+            self.upload_file_to_s3(JARS_PATH, jar, self.bucket, self.bucket_jars_dir)
+
+    def check_jars_on_s3(self, jars: List) -> None:
+        for jar in jars:
+            assert self.is_object_exists_on_s3(jar, self.bucket, self.bucket_jars_dir), \
+                f'{jar} not exists, please check.'
+
+    def upload_scripts_to_s3(self, scripts: List) -> None:
+        for script in scripts:
+            if self.is_object_exists_on_s3(script, self.bucket, self.bucket_scripts_dir):
+                logger.info(f'{script} already exists, skip upload it.')
+                continue
+            self.upload_file_to_s3(SCRIPTS_PATH, script, self.bucket, self.bucket_scripts_dir)
+
+    def upload_kylin_properties_to_s3(self, cluster_num: int = None, properties_file: str = 'kylin.properties') -> None:
+        # Always to upload local kylin.properties
+        local_dir = KYLIN_PROPERTIES_TEMPLATE_DIR.format(cluster_num=cluster_num if cluster_num else 'default')
+        bucket_dir = self.bucket_kylin_properties_of_other_cluster_dir.format(
+            cluster_num=cluster_num if cluster_num else 'default')
+        self.upload_file_to_s3(
+            local_file_dir=local_dir,
+            filename=properties_file,
+            bucket=self.bucket,
+            bucket_dir=bucket_dir)
+
+    def check_scripts_on_s3(self, scripts: List) -> None:
+        for script in scripts:
+            assert self.is_object_exists_on_s3(script, self.bucket, self.bucket_scripts_dir), \
+                f'{script} not exists, please check.'
+
+    def upload_file_to_s3(self, local_file_dir: str, filename: str, bucket: str, bucket_dir: str) -> None:
+        logger.info(f'Uploading {filename} from {local_file_dir} to S3 bucket: {bucket}/{bucket_dir}.')
+        self.s3_client.upload_file(os.path.join(local_file_dir, filename), bucket, bucket_dir + filename)
+        logger.info(f'Uploaded {filename} successfully.')
+
+    def is_stack_deleted_complete(self, stack_name: str) -> bool:
+        if self.is_stack_create_complete(stack_name):
+            # return directly if current stack status is 'CREATE_COMPLETE'
+            return False
+        if self._stack_delete_complete(stack_name):
+            return True
+        return False
+
+    def is_stack_create_complete(self, stack_name: str) -> bool:
+        return self._stack_status_check(name_or_id=stack_name, status='CREATE_COMPLETE')
+
+    def is_stack_delete_complete(self, stack_name: str) -> bool:
+        return self._stack_status_check(name_or_id=stack_name, status='DELETE_COMPLETE')
+
+    def is_stack_complete(self, stack_name: str) -> bool:
+        if self._stack_complete(stack_name):
+            return True
+        return False
+
+    def _validate_spark_worker_scale(self, stack_name: str) -> None:
+        if stack_name not in self.scaled_spark_workers_stacks:
+            msg = f'{stack_name} not in scaled list, please check.'
+            logger.error(msg)
+            raise Exception(msg)
+
+    def _validate_spark_worker_of_target_cluster_scale(self, stack_name: str, cluster_num: int) -> None:
+        if stack_name not in self.scaled_target_spark_workers_stacks(cluster_num):
+            msg = f'{stack_name} not in scaled list of target cluster {cluster_num}, please check.'
+            logger.error(msg)
+            raise Exception(msg)
+
+    def _validate_kylin_scale(self, stack_name: str) -> None:
+        if stack_name not in self.scaled_kylin_stacks:
+            msg = f'{stack_name} not in scaled list, please check.'
+            logger.error(msg)
+            raise Exception(msg)
+
+    def _validate_kylin_of_target_cluster_scale(self, stack_name: str, cluster_num: int) -> None:
+        if stack_name not in self.scaled_target_kylin_stacks(cluster_num):
+            msg = f'{stack_name} not in scaled list of target cluster {cluster_num}, please check.'
+            logger.error(msg)
+            raise Exception(msg)
+
+    def _stack_status_check(self, name_or_id: str, status: str) -> bool:
+        try:
+            resp: Dict = self.cf_client.describe_stacks(StackName=name_or_id)
+        except ClientError:
+            return False
+        return resp['Stacks'][0]['StackStatus'] == status
+
+    def _stack_complete(self, stack_name: str) -> bool:
+        try:
+            self.create_complete_waiter.wait(
+                StackName=stack_name,
+                WaiterConfig={
+                    'Delay': 30,
+                    'MaxAttempts': 120
+                }
+            )
+        except WaiterError as wx:
+            # logger.error(wx)
+            return False
+        return True
... 717 lines suppressed ...