diff --git a/ansible/group_vars/all.yml b/ansible/group_vars/all.yml
index 66fcda7c28..48a6bd82aa 100644
--- a/ansible/group_vars/all.yml
+++ b/ansible/group_vars/all.yml
@@ -317,6 +317,15 @@ skydive_agents_port: "8090"
 solum_application_deployment_port: "9777"
 solum_image_builder_port: "9778"
 
+storm_nimbus_thrift_port: 6627
+storm_supervisor_thrift_port: 6628
+# Storm will run up to (end - start) + 1 workers per worker host. Here
+# we reserve ports for those workers, and implicitly define the maximum
+# number of workers per host.
+storm_worker_port_range:
+  start: 6700
+  end: 6703
+
 swift_proxy_server_port: "8080"
 swift_object_server_port: "6000"
 swift_account_server_port: "6001"
@@ -515,6 +524,7 @@ enable_searchlight: "no"
 enable_senlin: "no"
 enable_skydive: "no"
 enable_solum: "no"
+enable_storm: "{{ enable_monasca | bool }}"
 enable_swift: "no"
 enable_tacker: "no"
 enable_telegraf: "no"
diff --git a/ansible/inventory/all-in-one b/ansible/inventory/all-in-one
index 87bd6ceb0b..4809020ab8 100644
--- a/ansible/inventory/all-in-one
+++ b/ansible/inventory/all-in-one
@@ -102,6 +102,9 @@ control
 [monasca:children]
 monitoring
 
+[storm:children]
+monitoring
+
 [mongodb:children]
 control
 
@@ -457,6 +460,13 @@ monasca
 [monasca-log-metrics:children]
 monasca
 
+# Storm
+[storm-worker:children]
+storm
+
+[storm-nimbus:children]
+storm
+
 # Ironic
 [ironic-api:children]
 ironic
diff --git a/ansible/inventory/multinode b/ansible/inventory/multinode
index 77216ca55c..10c7cb49d7 100644
--- a/ansible/inventory/multinode
+++ b/ansible/inventory/multinode
@@ -127,6 +127,9 @@ control
 [monasca:children]
 monitoring
 
+[storm:children]
+monitoring
+
 [mongodb:children]
 control
 
@@ -466,6 +469,13 @@ monasca
 [monasca-log-metrics:children]
 monasca
 
+# Storm
+[storm-worker:children]
+storm
+
+[storm-nimbus:children]
+storm
+
 # Ironic
 [ironic-api:children]
 ironic
diff --git a/ansible/roles/common/tasks/config.yml b/ansible/roles/common/tasks/config.yml
index e9934164df..2eb838f753 100644
--- a/ansible/roles/common/tasks/config.yml
+++ b/ansible/roles/common/tasks/config.yml
@@ -244,6 +244,7 @@
     - { name: "senlin", enabled: "{{ enable_senlin }}" }
     - { name: "skydive", enabled: "{{ enable_skydive }}" }
     - { name: "solum", enabled: "{{ enable_solum }}" }
+    - { name: "storm", enabled: "{{ enable_storm }}" }
     - { name: "swift", enabled: "{{ enable_swift }}" }
     - { name: "tacker", enabled: "{{ enable_tacker }}" }
     - { name: "tempest", enabled: "{{ enable_tempest }}" }
diff --git a/ansible/roles/common/templates/cron-logrotate-storm.conf.j2 b/ansible/roles/common/templates/cron-logrotate-storm.conf.j2
new file mode 100644
index 0000000000..a977476300
--- /dev/null
+++ b/ansible/roles/common/templates/cron-logrotate-storm.conf.j2
@@ -0,0 +1,3 @@
+"/var/log/kolla/storm/*.log"
+{
+}
diff --git a/ansible/roles/common/templates/cron.json.j2 b/ansible/roles/common/templates/cron.json.j2
index eca7cf76ad..63c0beee99 100644
--- a/ansible/roles/common/templates/cron.json.j2
+++ b/ansible/roles/common/templates/cron.json.j2
@@ -49,6 +49,7 @@
     ( 'senlin', enable_senlin ),
     ( 'skydive', enable_skydive ),
     ( 'solum', enable_solum ),
+    ( 'storm', enable_storm ),
     ( 'swift', enable_swift ),
     ( 'tacker', enable_tacker ),
     ( 'tempest', enable_tempest ),
diff --git a/ansible/roles/storm/defaults/main.yml b/ansible/roles/storm/defaults/main.yml
new file mode 100644
index 0000000000..bf69e3dad8
--- /dev/null
+++ b/ansible/roles/storm/defaults/main.yml
@@ -0,0 +1,47 @@
+---
+storm_services:
+  storm-worker:
+    container_name: storm_worker
+    group: storm-worker
+    enabled: true
+    image: "{{ storm_image_full }}"
+    environment:
+      STORM_LOG_DIR: /var/log/kolla/storm
+      STORM_LOG4J_PROP: "{{ storm_log_settings }}"
+    volumes:
+      - "{{ node_config_directory }}/storm-worker/:{{ container_config_directory }}/"
+      - "/etc/localtime:/etc/localtime:ro"
+      - "storm:/var/lib/storm/data"
+      - "kolla_logs:/var/log/kolla/"
+    dimensions: "{{ storm_worker_dimensions }}"
+  storm-nimbus:
+    container_name: storm_nimbus
+    group: storm-nimbus
+    enabled: true
+    image: "{{ storm_image_full }}"
+    environment:
+      STORM_LOG_DIR: /var/log/kolla/storm
+      STORM_LOG4J_PROP: "{{ storm_log_settings }}"
+    volumes:
+      - "{{ node_config_directory }}/storm-nimbus/:{{ container_config_directory }}/"
+      - "/etc/localtime:/etc/localtime:ro"
+      - "storm:/var/lib/storm/data"
+      - "kolla_logs:/var/log/kolla/"
+    dimensions: "{{ storm_nimbus_dimensions }}"
+
+####################
+# Storm
+####################
+storm_log_settings: 'INFO,ROLLINGFILE'
+storm_nimbus_servers: "{% for host in groups['storm-nimbus'] %}'{{ hostvars[host]['ansible_' + hostvars[host]['api_interface']]['ipv4']['address'] }}'{% if not loop.last %},{% endif %}{% endfor %}"
+
+####################
+# Docker
+####################
+storm_install_type: "{{ kolla_install_type }}"
+storm_image: "{{ docker_registry ~ '/' if docker_registry else '' }}{{ docker_namespace }}/{{ kolla_base_distro }}-{{ storm_install_type }}-storm"
+storm_tag: "{{ openstack_release }}"
+storm_image_full: "{{ storm_image }}:{{ storm_tag }}"
+
+storm_worker_dimensions: "{{ default_container_dimensions }}"
+storm_nimbus_dimensions: "{{ default_container_dimensions }}"
diff --git a/ansible/roles/storm/handlers/main.yml b/ansible/roles/storm/handlers/main.yml
new file mode 100644
index 0000000000..c03afb73e9
--- /dev/null
+++ b/ansible/roles/storm/handlers/main.yml
@@ -0,0 +1,44 @@
+---
+- name: Restart storm-worker container
+  vars:
+    service_name: "storm-worker"
+    service: "{{ storm_services[service_name] }}"
+    config_json: "{{ storm_config_jsons.results|selectattr('item.key', 'equalto', service_name)|first }}"
+    worker_container: "{{ check_storm_containers.results|selectattr('item.key', 'equalto', service_name)|first }}"
+  kolla_docker:
+    action: "recreate_or_restart_container"
+    common_options: "{{ docker_common_options }}"
+    name: "{{ service.container_name }}"
+    image: "{{ service.image }}"
+    environment: "{{ service.environment }}"
+    volumes: "{{ service.volumes }}"
+    dimensions: "{{ service.dimensions }}"
+  when:
+    - kolla_action != "config"
+    - inventory_hostname in groups[service.group]
+    - service.enabled | bool
+    - config_json.changed | bool
+      or storm_worker_conf.changed | bool
+      or worker_container.changed | bool
+
+- name: Restart storm-nimbus container
+  vars:
+    service_name: "storm-nimbus"
+    service: "{{ storm_services[service_name] }}"
+    config_json: "{{ storm_config_jsons.results|selectattr('item.key', 'equalto', service_name)|first }}"
+    nimbus_container: "{{ check_storm_containers.results|selectattr('item.key', 'equalto', service_name)|first }}"
+  kolla_docker:
+    action: "recreate_or_restart_container"
+    common_options: "{{ docker_common_options }}"
+    name: "{{ service.container_name }}"
+    image: "{{ service.image }}"
+    environment: "{{ service.environment }}"
+    volumes: "{{ service.volumes }}"
+    dimensions: "{{ service.dimensions }}"
+  when:
+    - kolla_action != "config"
+    - inventory_hostname in groups[service.group]
+    - service.enabled | bool
+    - config_json.changed | bool
+      or storm_nimbus_conf.changed | bool
+      or nimbus_container.changed | bool
diff --git a/ansible/roles/storm/meta/main.yml b/ansible/roles/storm/meta/main.yml
new file mode 100644
index 0000000000..6b4fff8fef
--- /dev/null
+++ b/ansible/roles/storm/meta/main.yml
@@ -0,0 +1,3 @@
+---
+dependencies:
+  - { role: common }
diff --git a/ansible/roles/storm/tasks/check.yml b/ansible/roles/storm/tasks/check.yml
new file mode 100644
index 0000000000..ed97d539c0
--- /dev/null
+++ b/ansible/roles/storm/tasks/check.yml
@@ -0,0 +1 @@
+---
diff --git a/ansible/roles/storm/tasks/config.yml b/ansible/roles/storm/tasks/config.yml
new file mode 100644
index 0000000000..d3238267c5
--- /dev/null
+++ b/ansible/roles/storm/tasks/config.yml
@@ -0,0 +1,84 @@
+---
+- name: Ensuring config directories exist
+  file:
+    path: "{{ node_config_directory }}/{{ item.key }}"
+    state: "directory"
+    owner: "{{ config_owner_user }}"
+    group: "{{ config_owner_group }}"
+    mode: "0770"
+    recurse: yes
+  become: true
+  when:
+    - inventory_hostname in groups[item.value.group]
+    - item.value.enabled | bool
+  with_dict: "{{ storm_services }}"
+
+- name: Copying over config.json files for services
+  template:
+    src: "{{ item.key }}.json.j2"
+    dest: "{{ node_config_directory }}/{{ item.key }}/config.json"
+    mode: "0660"
+  become: true
+  register: storm_config_jsons
+  when:
+    - inventory_hostname in groups[item.value.group]
+    - item.value.enabled | bool
+  with_dict: "{{ storm_services }}"
+  notify:
+    - "Restart {{ item.key }} container"
+
+- name: Copying over storm worker config
+  vars:
+    service: "{{ storm_services['storm-worker'] }}"
+  template:
+    src: "{{ item }}"
+    dest: "{{ node_config_directory }}/storm-worker/storm.yml"
+    mode: "0660"
+  become: true
+  register: storm_worker_conf
+  with_first_found:
+    - "{{ node_custom_config }}/storm/{{ inventory_hostname }}/storm.yml"
+    - "{{ node_custom_config }}/storm.yml"
+    - "storm.yml.j2"
+  when:
+    - inventory_hostname in groups[service['group']]
+    - service.enabled | bool
+  notify:
+    - Restart storm-worker container
+
+- name: Copying over storm nimbus config
+  vars:
+    service: "{{ storm_services['storm-nimbus'] }}"
+  template:
+    src: "{{ item }}"
+    dest: "{{ node_config_directory }}/storm-nimbus/storm.yml"
+    mode: "0660"
+  become: true
+  register: storm_nimbus_conf
+  with_first_found:
+    - "{{ node_custom_config }}/storm/{{ inventory_hostname }}/storm.yml"
+    - "{{ node_custom_config }}/storm.yml"
+    - "storm.yml.j2"
+  when:
+    - inventory_hostname in groups[service['group']]
+    - service.enabled | bool
+  notify:
+    - Restart storm-nimbus container
+
+- name: Check storm containers
+  kolla_docker:
+    action: "compare_container"
+    common_options: "{{ docker_common_options }}"
+    name: "{{ item.value.container_name }}"
+    image: "{{ item.value.image }}"
+    volumes: "{{ item.value.volumes }}"
+    environment: "{{ item.value.environment }}"
+    dimensions: "{{ item.value.dimensions }}"
+  register: check_storm_containers
+  when:
+    - kolla_action != "config"
+    - inventory_hostname in groups[item.value.group]
+    - item.value.enabled | bool
+  with_dict: "{{ storm_services }}"
+  notify:
+    - "Restart {{ item.key }} container"
diff --git a/ansible/roles/storm/tasks/deploy.yml b/ansible/roles/storm/tasks/deploy.yml
new file mode 100644
index 0000000000..dd26ecc34d
--- /dev/null
+++ b/ansible/roles/storm/tasks/deploy.yml
@@ -0,0 +1,5 @@
+---
+- include: config.yml
+
+- name: Flush handlers
+  meta: flush_handlers
diff --git a/ansible/roles/storm/tasks/main.yml b/ansible/roles/storm/tasks/main.yml
new file mode 100644
index 0000000000..49a33b8492
--- /dev/null
+++ b/ansible/roles/storm/tasks/main.yml
@@ -0,0 +1,2 @@
+---
+- include: "{{ kolla_action }}.yml"
diff --git a/ansible/roles/storm/tasks/precheck.yml b/ansible/roles/storm/tasks/precheck.yml
new file mode 100644
index 0000000000..91c66e18bd
--- /dev/null
+++ b/ansible/roles/storm/tasks/precheck.yml
@@ -0,0 +1,41 @@
+---
+- name: Get container facts
+  kolla_container_facts:
+    name:
+      - storm_worker
+      - storm_nimbus
+  register: container_facts
+
+- name: Checking storm nimbus thrift port is available
+  wait_for:
+    host: "{{ api_interface_address }}"
+    port: "{{ storm_nimbus_thrift_port }}"
+    connect_timeout: 1
+    timeout: 1
+    state: stopped
+  when:
+    - container_facts['storm_nimbus'] is not defined
+    - inventory_hostname in groups['storm-nimbus']
+
+- name: Checking storm supervisor thrift port is available
+  wait_for:
+    host: "{{ api_interface_address }}"
+    port: "{{ storm_supervisor_thrift_port }}"
+    connect_timeout: 1
+    timeout: 1
+    state: stopped
+  when:
+    - container_facts['storm_worker'] is not defined
+    - inventory_hostname in groups['storm-worker']
+
+- name: Checking storm worker ports are available
+  wait_for:
+    host: "{{ api_interface_address }}"
+    port: "{{ item }}"
+    connect_timeout: 1
+    timeout: 1
+    state: stopped
+  with_sequence: "start={{ storm_worker_port_range.start|int }} end={{ storm_worker_port_range.end|int }}"
+  when:
+    - container_facts['storm_worker'] is not defined
+    - inventory_hostname in groups['storm-worker']
diff --git a/ansible/roles/storm/tasks/pull.yml b/ansible/roles/storm/tasks/pull.yml
new file mode 100644
index 0000000000..757d0477c9
--- /dev/null
+++ b/ansible/roles/storm/tasks/pull.yml
@@ -0,0 +1,10 @@
+---
+- name: Pulling storm images
+  kolla_docker:
+    action: "pull_image"
+    common_options: "{{ docker_common_options }}"
+    image: "{{ item.value.image }}"
+  when:
+    - inventory_hostname in groups[item.value.group]
+    - item.value.enabled | bool
+  with_dict: "{{ storm_services }}"
diff --git a/ansible/roles/storm/tasks/reconfigure.yml b/ansible/roles/storm/tasks/reconfigure.yml
new file mode 100644
index 0000000000..e078ef1318
--- /dev/null
+++ b/ansible/roles/storm/tasks/reconfigure.yml
@@ -0,0 +1,2 @@
+---
+- include: deploy.yml
diff --git a/ansible/roles/storm/tasks/upgrade.yml b/ansible/roles/storm/tasks/upgrade.yml
new file mode 100644
index 0000000000..e078ef1318
--- /dev/null
+++ b/ansible/roles/storm/tasks/upgrade.yml
@@ -0,0 +1,2 @@
+---
+- include: deploy.yml
diff --git a/ansible/roles/storm/templates/storm-nimbus.json.j2 b/ansible/roles/storm/templates/storm-nimbus.json.j2
new file mode 100644
index 0000000000..5ceaeafd2c
--- /dev/null
+++ b/ansible/roles/storm/templates/storm-nimbus.json.j2
@@ -0,0 +1,23 @@
+{
+    "command": "/opt/storm/bin/storm nimbus",
+    "config_files": [
+        {
+            "source": "{{ container_config_directory }}/storm.yml",
+            "dest": "/opt/storm/conf/storm.yaml",
+            "owner": "storm",
+            "perm": "0600"
+        }
+    ],
+    "permissions": [
+        {
+            "path": "/var/lib/storm",
+            "owner": "storm:storm",
+            "recurse": true
+        },
+        {
+            "path": "/var/log/kolla/storm",
+            "owner": "storm:storm",
+            "recurse": true
+        }
+    ]
+}
diff --git a/ansible/roles/storm/templates/storm-worker.json.j2 b/ansible/roles/storm/templates/storm-worker.json.j2
new file mode 100644
index 0000000000..564b76ab9f
--- /dev/null
+++ b/ansible/roles/storm/templates/storm-worker.json.j2
@@ -0,0 +1,23 @@
+{
+    "command": "/opt/storm/bin/storm supervisor",
+    "config_files": [
+        {
+            "source": "{{ container_config_directory }}/storm.yml",
+            "dest": "/opt/storm/conf/storm.yaml",
+            "owner": "storm",
+            "perm": "0600"
+        }
+    ],
+    "permissions": [
+        {
+            "path": "/var/lib/storm",
+            "owner": "storm:storm",
+            "recurse": true
+        },
+        {
+            "path": "/var/log/kolla/storm",
+            "owner": "storm:storm",
+            "recurse": true
+        }
+    ]
+}
diff --git a/ansible/roles/storm/templates/storm.yml.j2 b/ansible/roles/storm/templates/storm.yml.j2
new file mode 100644
index 0000000000..965dd11796
--- /dev/null
+++ b/ansible/roles/storm/templates/storm.yml.j2
@@ -0,0 +1,14 @@
+storm.local.dir: "/var/lib/storm/data"
+storm.log.dir: "/var/log/kolla/storm"
+nimbus.seeds: [{{ storm_nimbus_servers }}]
+storm.zookeeper.port: {{ zookeeper_client_port }}
+storm.zookeeper.servers:
+{% for host in groups['zookeeper'] %}
+  - "{{ hostvars[host]['ansible_' + hostvars[host]['api_interface']]['ipv4']['address'] }}"
+{% endfor %}
+supervisor.slots.ports:
+{% for port in range(storm_worker_port_range.start|int, storm_worker_port_range.end|int + 1) %}
+  - {{ port }}
+{% endfor %}
+supervisor.thrift.port: {{ storm_supervisor_thrift_port }}
+nimbus.thrift.port: {{ storm_nimbus_thrift_port }}
diff --git a/ansible/site.yml b/ansible/site.yml
index 694d3f7e40..1616eabb51 100644
--- a/ansible/site.yml
+++ b/ansible/site.yml
@@ -517,6 +517,14 @@
         tags: kafka,
         when: enable_kafka | bool }
 
+- name: Apply role storm
+  hosts: storm
+  serial: '{{ kolla_serial|default("0") }}'
+  roles:
+    - { role: storm,
+        tags: storm,
+        when: enable_storm | bool }
+
 - name: Apply role karbor
   gather_facts: false
   hosts: karbor
diff --git a/etc/kolla/globals.yml b/etc/kolla/globals.yml
index c59614f4eb..382235e5b9 100644
--- a/etc/kolla/globals.yml
+++ b/etc/kolla/globals.yml
@@ -268,6 +268,7 @@ kolla_internal_vip_address: "10.10.10.254"
 #enable_skydive: "no"
 #enable_solum: "no"
 #enable_swift: "no"
+#enable_storm: "no"
 #enable_telegraf: "no"
 #enable_tacker: "no"
 #enable_tempest: "no"