diff --git a/ansible/group_vars/all.yml b/ansible/group_vars/all.yml
index 69a42f919b..5ad218ab8d 100644
--- a/ansible/group_vars/all.yml
+++ b/ansible/group_vars/all.yml
@@ -210,6 +210,8 @@ ironic_inspector_port: "5050"
 
 iscsi_port: "3260"
 
+kafka_port: "9092"
+
 karbor_api_port: "8799"
 
 keystone_public_port: "5000"
@@ -441,6 +443,7 @@ enable_ironic: "no"
 enable_ironic_pxe_uefi: "no"
 enable_iscsid: "{{ (enable_cinder | bool and enable_cinder_backend_iscsi | bool) or enable_ironic | bool }}"
 enable_karbor: "no"
+enable_kafka: "no"
 enable_kuryr: "no"
 enable_magnum: "no"
 enable_manila: "no"
@@ -487,7 +490,7 @@ enable_trove_singletenant: "no"
 enable_vitrage: "no"
 enable_vmtp: "no"
 enable_watcher: "no"
-enable_zookeeper: "no"
+enable_zookeeper: "{{ enable_kafka | bool }}"
 enable_zun: "no"
 
 ovs_datapath: "{{ 'netdev' if enable_ovs_dpdk | bool else 'system' }}"
diff --git a/ansible/inventory/all-in-one b/ansible/inventory/all-in-one
index ffa40675d8..89ef1c8a21 100644
--- a/ansible/inventory/all-in-one
+++ b/ansible/inventory/all-in-one
@@ -56,6 +56,9 @@ monitoring
 control
 compute
 
+[kafka:children]
+control
+
 [karbor:children]
 control
 
diff --git a/ansible/inventory/multinode b/ansible/inventory/multinode
index 61918913e6..b19f84a02b 100644
--- a/ansible/inventory/multinode
+++ b/ansible/inventory/multinode
@@ -78,6 +78,9 @@ compute
 [influxdb:children]
 monitoring
 
+[kafka:children]
+control
+
 [karbor:children]
 control
 
diff --git a/ansible/roles/common/tasks/config.yml b/ansible/roles/common/tasks/config.yml
index 36b9b08f99..bc5ec7d9f5 100644
--- a/ansible/roles/common/tasks/config.yml
+++ b/ansible/roles/common/tasks/config.yml
@@ -52,6 +52,7 @@
     - "04-openstack-wsgi"
     - "05-libvirt"
     - "06-zookeeper"
+    - "07-kafka"
   notify:
     - Restart fluentd container
 
@@ -193,6 +194,7 @@
     - { name: "ironic", enabled: "{{ enable_ironic }}" }
     - { name: "ironic-inspector", enabled: "{{ enable_ironic }}" }
     - { name: "iscsid", enabled: "{{ enable_iscsid }}" }
+    - { name: "kafka", enabled: "{{ enable_kafka }}" }
     - { name: "karbor", enabled: "{{ enable_karbor }}" }
     - { name: "keepalived", enabled: "{{ enable_haproxy }}" }
     - { name: "keystone", enabled: "{{ enable_keystone }}" }
diff --git a/ansible/roles/common/templates/conf/input/07-kafka.conf.j2 b/ansible/roles/common/templates/conf/input/07-kafka.conf.j2
new file mode 100644
index 0000000000..15a2ee9205
--- /dev/null
+++ b/ansible/roles/common/templates/conf/input/07-kafka.conf.j2
@@ -0,0 +1,11 @@
+{% set fluentd_dir = 'td-agent' if kolla_base_distro in ['ubuntu', 'debian'] else 'fluentd' %}
+<source>
+  @type tail
+  path /var/log/kolla/kafka/controller.log, /var/log/kolla/kafka/server.log, /var/log/kolla/kafka/state-change.log
+  pos_file /var/run/{{ fluentd_dir }}/kafka.pos
+  tag infra.*
+  format multiline
+  format_firstline /^\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3}\] \S+ .*$/
+  format1 /^\[(?<Timestamp>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3})\] (?<log_level>\S+) (?<Payload>.*)$/
+  time_key Timestamp
+</source>
diff --git a/ansible/roles/common/templates/cron-logrotate-kafka.conf.j2 b/ansible/roles/common/templates/cron-logrotate-kafka.conf.j2
new file mode 100644
index 0000000000..79545c60ea
--- /dev/null
+++ b/ansible/roles/common/templates/cron-logrotate-kafka.conf.j2
@@ -0,0 +1,3 @@
+"/var/log/kolla/kafka/*.log"
+{
+}
diff --git a/ansible/roles/common/templates/cron.json.j2 b/ansible/roles/common/templates/cron.json.j2
index bfe3c4c1c4..3ee2fb0c54 100644
--- a/ansible/roles/common/templates/cron.json.j2
+++ b/ansible/roles/common/templates/cron.json.j2
@@ -24,6 +24,7 @@
     ( 'ironic', enable_ironic ),
     ( 'ironic-inspector', enable_ironic ),
     ( 'iscsid', enable_iscsid ),
+    ( 'kafka', enable_kafka ),
     ( 'karbor', enable_karbor ),
     ( 'keepalived', enable_haproxy ),
     ( 'keystone', enable_keystone ),
diff --git a/ansible/roles/common/templates/fluentd.json.j2 b/ansible/roles/common/templates/fluentd.json.j2
index c622b0a664..55a3f37c0d 100644
--- a/ansible/roles/common/templates/fluentd.json.j2
+++ b/ansible/roles/common/templates/fluentd.json.j2
@@ -53,6 +53,12 @@
             "owner": "{{ fluentd_user }}",
             "perm": "0600"
         },
+        {
+            "source": "{{ container_config_directory }}/input/07-kafka.conf",
+            "dest": "{{ fluentd_dir }}/input/07-kafka.conf",
+            "owner": "{{ fluentd_user }}",
+            "perm": "0600"
+        },
         {# Copy all configuration files in filter/ directory to include #}
         {# custom filter configs. #}
         {
diff --git a/ansible/roles/kafka/defaults/main.yml b/ansible/roles/kafka/defaults/main.yml
new file mode 100644
index 0000000000..ecf13665cb
--- /dev/null
+++ b/ansible/roles/kafka/defaults/main.yml
@@ -0,0 +1,31 @@
+---
+kafka_services:
+  kafka:
+    container_name: kafka
+    group: kafka
+    enabled: true
+    image: "{{ kafka_image_full }}"
+    environment:
+      LOG_DIR: "{{ kafka_log_dir }}"
+      KAFKA_HEAP_OPTS: "{{ kafka_heap_opts }}"
+    volumes:
+      - "{{ node_config_directory }}/kafka/:{{ container_config_directory }}/"
+      - "/etc/localtime:/etc/localtime:ro"
+      - "kafka:/var/lib/kafka/data"
+      - "kolla_logs:/var/log/kolla/"
+
+####################
+# Kafka
+####################
+kafka_cluster_name: "kolla_kafka"
+kafka_log_dir: "/var/log/kolla/kafka"
+kafka_heap_opts: "-Xmx1G -Xms1G"
+kafka_zookeeper: "{% for host in groups['zookeeper'] %}{{ hostvars[host]['ansible_' + hostvars[host]['api_interface']]['ipv4']['address'] }}:{{ zookeeper_client_port }}{% if not loop.last %},{% endif %}{% endfor %}"
+
+####################
+# Docker
+####################
+kafka_install_type: "{{ kolla_install_type }}"
+kafka_image: "{{ docker_registry ~ '/' if docker_registry else '' }}{{ docker_namespace }}/{{ kolla_base_distro }}-{{ kafka_install_type }}-kafka"
+kafka_tag: "{{ openstack_release }}"
+kafka_image_full: "{{ kafka_image }}:{{ kafka_tag }}"
diff --git a/ansible/roles/kafka/handlers/main.yml b/ansible/roles/kafka/handlers/main.yml
new file mode 100644
index 0000000000..caf1100ec7
--- /dev/null
+++ b/ansible/roles/kafka/handlers/main.yml
@@ -0,0 +1,22 @@
+---
+- name: Restart kafka container
+  vars:
+    service_name: "kafka"
+    service: "{{ kafka_services[service_name] }}"
+    config_json: "{{ kafka_config_jsons.results|selectattr('item.key', 'equalto', service_name)|first }}"
+    kafka_conf: "{{ kafka_confs.results|selectattr('item.key', 'equalto', service_name)|first }}"
+    kafka_container: "{{ check_kafka_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 }}"
+  when:
+    - action != "config"
+    - inventory_hostname in groups[service.group]
+    - service.enabled | bool
+    - config_json.changed | bool
+      or kafka_conf.changed | bool
+      or kafka_container.changed | bool
diff --git a/ansible/roles/kafka/meta/main.yml b/ansible/roles/kafka/meta/main.yml
new file mode 100644
index 0000000000..6b4fff8fef
--- /dev/null
+++ b/ansible/roles/kafka/meta/main.yml
@@ -0,0 +1,3 @@
+---
+dependencies:
+  - { role: common }
diff --git a/ansible/roles/kafka/tasks/check.yml b/ansible/roles/kafka/tasks/check.yml
new file mode 100644
index 0000000000..ed97d539c0
--- /dev/null
+++ b/ansible/roles/kafka/tasks/check.yml
@@ -0,0 +1 @@
+---
diff --git a/ansible/roles/kafka/tasks/config.yml b/ansible/roles/kafka/tasks/config.yml
new file mode 100644
index 0000000000..ab34244577
--- /dev/null
+++ b/ansible/roles/kafka/tasks/config.yml
@@ -0,0 +1,62 @@
+---
+- 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: "{{ kafka_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: kafka_config_jsons
+  when:
+    - inventory_hostname in groups[item.value.group]
+    - item.value.enabled | bool
+  with_dict: "{{ kafka_services }}"
+  notify:
+    - Restart kafka container
+
+- name: Copying over kafka config
+  merge_configs:
+    sources:
+      - "{{ role_path }}/templates/kafka.server.properties.j2"
+      - "{{ node_custom_config }}/kafka.server.properties"
+      - "{{ node_custom_config }}/{{ item.key }}/{{ inventory_hostname }}/kafka.server.properties"
+    dest: "{{ node_config_directory }}/{{ item.key }}/kafka.server.properties"
+    mode: "0660"
+  become: true
+  register: kafka_confs
+  when:
+    - inventory_hostname in groups[item.value.group]
+    - item.value.enabled | bool
+  with_dict: "{{ kafka_services }}"
+  notify:
+    - Restart kafka container
+
+- name: Check kafka 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 }}"
+  register: check_kafka_containers
+  when:
+    - action != "config"
+    - inventory_hostname in groups[item.value.group]
+    - item.value.enabled | bool
+  with_dict: "{{ kafka_services }}"
+  notify:
+    - Restart kafka container
diff --git a/ansible/roles/kafka/tasks/deploy.yml b/ansible/roles/kafka/tasks/deploy.yml
new file mode 100644
index 0000000000..dd26ecc34d
--- /dev/null
+++ b/ansible/roles/kafka/tasks/deploy.yml
@@ -0,0 +1,5 @@
+---
+- include: config.yml
+
+- name: Flush handlers
+  meta: flush_handlers
diff --git a/ansible/roles/kafka/tasks/main.yml b/ansible/roles/kafka/tasks/main.yml
new file mode 100644
index 0000000000..b017e8b4ad
--- /dev/null
+++ b/ansible/roles/kafka/tasks/main.yml
@@ -0,0 +1,2 @@
+---
+- include: "{{ action }}.yml"
diff --git a/ansible/roles/kafka/tasks/precheck.yml b/ansible/roles/kafka/tasks/precheck.yml
new file mode 100644
index 0000000000..924d393319
--- /dev/null
+++ b/ansible/roles/kafka/tasks/precheck.yml
@@ -0,0 +1,17 @@
+---
+- name: Get container facts
+  kolla_container_facts:
+    name:
+      - kafka
+  register: container_facts
+
+- name: Checking free port for Kafka
+  wait_for:
+    host: "{{ api_interface_address }}"
+    port: "{{ kafka_port }}"
+    connect_timeout: 1
+    timeout: 1
+    state: stopped
+  when:
+    - container_facts['kafka'] is not defined
+    - inventory_hostname in groups['kafka']
diff --git a/ansible/roles/kafka/tasks/pull.yml b/ansible/roles/kafka/tasks/pull.yml
new file mode 100644
index 0000000000..83f2aa6b18
--- /dev/null
+++ b/ansible/roles/kafka/tasks/pull.yml
@@ -0,0 +1,10 @@
+---
+- name: Pulling kafka 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: "{{ kafka_services }}"
diff --git a/ansible/roles/kafka/tasks/reconfigure.yml b/ansible/roles/kafka/tasks/reconfigure.yml
new file mode 100644
index 0000000000..e078ef1318
--- /dev/null
+++ b/ansible/roles/kafka/tasks/reconfigure.yml
@@ -0,0 +1,2 @@
+---
+- include: deploy.yml
diff --git a/ansible/roles/kafka/tasks/upgrade.yml b/ansible/roles/kafka/tasks/upgrade.yml
new file mode 100644
index 0000000000..dd26ecc34d
--- /dev/null
+++ b/ansible/roles/kafka/tasks/upgrade.yml
@@ -0,0 +1,5 @@
+---
+- include: config.yml
+
+- name: Flush handlers
+  meta: flush_handlers
diff --git a/ansible/roles/kafka/templates/kafka.json.j2 b/ansible/roles/kafka/templates/kafka.json.j2
new file mode 100644
index 0000000000..7f83d55595
--- /dev/null
+++ b/ansible/roles/kafka/templates/kafka.json.j2
@@ -0,0 +1,23 @@
+{
+    "command": "/opt/kafka/bin/kafka-server-start.sh /etc/kafka/kafka.server.properties",
+    "config_files": [
+        {
+            "source": "{{ container_config_directory }}/kafka.server.properties",
+            "dest": "/etc/kafka/kafka.server.properties",
+            "owner": "kafka",
+            "perm": "0600"
+        }
+    ],
+    "permissions": [
+        {
+            "path": "/var/lib/kafka",
+            "owner": "kafka:kafka",
+            "recurse": true
+        },
+        {
+            "path": "/var/log/kolla/kafka",
+            "owner": "kafka:kafka",
+            "recurse": true
+        }
+    ]
+}
diff --git a/ansible/roles/kafka/templates/kafka.server.properties.j2 b/ansible/roles/kafka/templates/kafka.server.properties.j2
new file mode 100644
index 0000000000..a8ca4465ed
--- /dev/null
+++ b/ansible/roles/kafka/templates/kafka.server.properties.j2
@@ -0,0 +1,17 @@
+listeners=PLAINTEXT://{{ api_interface_address }}:{{ kafka_port }}
+num.network.threads=3
+num.io.threads=8
+socket.send.buffer.bytes=102400
+socket.receive.buffer.bytes=102400
+socket.request.max.bytes=104857600
+log.dirs=/var/lib/kafka/data
+num.partitions=1
+num.recovery.threads.per.data.dir=1
+offsets.topic.replication.factor=3
+transaction.state.log.replication.factor=3
+transaction.state.log.min.isr=3
+log.retention.hours=168
+log.segment.bytes=1073741824
+log.retention.check.interval.ms=300000
+zookeeper.connect={{ kafka_zookeeper }}
+zookeeper.connection.timeout.ms=6000
diff --git a/ansible/site.yml b/ansible/site.yml
index 65baeca0e3..2ec6cec9d9 100644
--- a/ansible/site.yml
+++ b/ansible/site.yml
@@ -253,6 +253,15 @@
         tags: ceph,
         when: enable_ceph | bool }
 
+- name: Apply role kafka
+  gather_facts: false
+  hosts: kafka
+  serial: '{{ serial|default("0") }}'
+  roles:
+    - { role: kafka,
+        tags: kafka,
+        when: enable_kafka | bool }
+
 - name: Apply role karbor
   gather_facts: false
   hosts: karbor
diff --git a/etc/kolla/globals.yml b/etc/kolla/globals.yml
index 3f02dab982..d0a78c7a5b 100644
--- a/etc/kolla/globals.yml
+++ b/etc/kolla/globals.yml
@@ -190,6 +190,7 @@ kolla_internal_vip_address: "10.10.10.254"
 #enable_influxdb: "no"
 #enable_ironic: "no"
 #enable_ironic_pxe_uefi: "no"
+#enable_kafka: "no"
 #enable_karbor: "no"
 #enable_kuryr: "no"
 #enable_magnum: "no"
diff --git a/releasenotes/notes/add-kafka-role-ec7a9def49e06e51.yaml b/releasenotes/notes/add-kafka-role-ec7a9def49e06e51.yaml
new file mode 100644
index 0000000000..e0437961ea
--- /dev/null
+++ b/releasenotes/notes/add-kafka-role-ec7a9def49e06e51.yaml
@@ -0,0 +1,5 @@
+---
+features:
+  - Add a role for deploying Apache Kafka, a distributed streaming platform.
+    See https://kafka.apache.org/ for more details. Requires Apache Zookeeper
+    to be configured.