diff --git a/playbooks/bridge.yaml b/playbooks/bridge.yaml
index 5c59682a00..75f4758a30 100644
--- a/playbooks/bridge.yaml
+++ b/playbooks/bridge.yaml
@@ -5,3 +5,4 @@
     - install-ansible
     - root-keys
     - ansible-cron
+    - cloud-launcher-cron
diff --git a/playbooks/roles/cloud-launcher-cron/README.rst b/playbooks/roles/cloud-launcher-cron/README.rst
new file mode 100644
index 0000000000..488efed0e6
--- /dev/null
+++ b/playbooks/roles/cloud-launcher-cron/README.rst
@@ -0,0 +1,26 @@
+Setup periodic runs of ``run_cloud_launcher.sh``, which runs the cloud setup
+playbook against our clouds.
+
+Note that this runs in an independent cron beacuse we don't need to run it
+as frequently as our normal ansible runs and this ansible process needs
+access to the all-clouds.yaml file which we don't run the normal ansible runs
+with.
+
+**Role Variables**
+
+.. zuul:rolevar:: cloud_launcher_cron_interval
+
+   .. zuul:rolevar:: minute
+      :default: 0
+
+   .. zuul:rolevar:: hour
+      :default: */1
+
+   .. zuul:rolevar:: day
+      :default: *
+
+   .. zuul:rolevar:: month
+      :default: *
+
+   .. zuul:rolevar:: weekday
+      :default: *
diff --git a/playbooks/roles/cloud-launcher-cron/defaults/main.yaml b/playbooks/roles/cloud-launcher-cron/defaults/main.yaml
new file mode 100644
index 0000000000..344859ba46
--- /dev/null
+++ b/playbooks/roles/cloud-launcher-cron/defaults/main.yaml
@@ -0,0 +1,6 @@
+cloud_launcher_cron_interval:
+  minute: '0'
+  hour: '*/1'
+  day: '*'
+  month: '*'
+  weekday: '*'
diff --git a/playbooks/roles/cloud-launcher-cron/tasks/main.yaml b/playbooks/roles/cloud-launcher-cron/tasks/main.yaml
new file mode 100644
index 0000000000..628d032f5f
--- /dev/null
+++ b/playbooks/roles/cloud-launcher-cron/tasks/main.yaml
@@ -0,0 +1,21 @@
+- name: Ensure directory exists for lock files
+  file:
+    state: directory
+    path: /var/run/ansible
+
+- name: Set up cron job for running run_cloud_launcher.sh
+  cron:
+    name: run_cloud_launcher.sh
+    state: present
+    job: '/usr/bin/flock -n /var/run/ansible/run_cloud_launcher.lock /bin/bash /opt/system-config/run_cloud_launcher.sh >> /var/log/ansible/run_cloud_launcher_cron.log 2>&1'
+    minute: "{{ cloud_launcher_cron_interval.minute }}"
+    hour: "{{ cloud_launcher_cron_interval.hour }}"
+    day: "{{ cloud_launcher_cron_interval.day }}"
+    month: "{{ cloud_launcher_cron_interval.month }}"
+    weekday: "{{ cloud_launcher_cron_interval.weekday }}"
+
+- name: Setup log rotation
+  include_role:
+    name: logrotate
+  vars:
+    logrotate_file_name: /var/log/ansible/run_cloud_launcher_cron.log
diff --git a/run_cloud_launcher.sh b/run_cloud_launcher.sh
index 538dcd392d..b3fcaf7f0b 100755
--- a/run_cloud_launcher.sh
+++ b/run_cloud_launcher.sh
@@ -32,6 +32,6 @@ export OS_CLIENT_CONFIG_FILE=/etc/openstack/all-clouds.yaml
 
 # Pass -i /dev/null to avoid the ansible-playbook run with all-clouds.yaml
 # being active messing with the normal inventory cache.
-timeout -k 2m 120m ansible-playbook -i /dev/null -f 1 \
+/usr/bin/timeout -k 2m 120m /usr/local/bin/ansible-playbook -i /dev/null -f 1 \
     ${ANSIBLE_PLAYBOOKS}/run_cloud_launcher.yaml \
     -e@${ANSIBLE_PLAYBOOKS}/clouds_layouts.yml
diff --git a/testinfra/test_bridge.py b/testinfra/test_bridge.py
index 439cc6355f..8964c4fd86 100644
--- a/testinfra/test_bridge.py
+++ b/testinfra/test_bridge.py
@@ -43,3 +43,9 @@ def test_openstacksdk_config(host):
     assert f.user == 'root'
     assert f.group == 'root'
     assert f.mode == 0o640
+
+
+def test_cloud_launcher_cron(host):
+    with host.sudo():
+        crontab = host.check_output('crontab -l')
+        assert 'run_cloud_launcher.sh' in crontab