/
/
/
Ansible role that deployes services on my runner machine
1---
2# Runner Services - Unified LLM Stack Deployment
3# Single deployment with Ollama, OpenWebUI, and LiteLLM services
4
5- name: Check if LLM stack is enabled
6 fail:
7 msg: "LLM stack is disabled. Set llm_stack_enabled: true to enable."
8 when: not llm_stack_enabled
9 tags: always
10
11- name: Create LLM stack configuration directory structure
12 file:
13 path: "{{ item }}"
14 state: directory
15 owner: "{{ runner_user }}"
16 group: "{{ runner_group }}"
17 mode: '0775'
18 loop:
19 - "{{ llm_stack_config_dir }}"
20 - "{{ llm_stack_ollama_data_dir }}"
21 - "{{ llm_stack_openwebui_data_dir }}"
22 - "{{ llm_stack_litellm_data_dir }}"
23
24- name: Set group sticky bit on LLM stack directories for permission inheritance
25 file:
26 path: "{{ llm_stack_config_dir }}"
27 state: directory
28 mode: "g+s"
29
30- name: Create unified LLM stack Docker Compose file
31 template:
32 src: llm-stack-compose.yml.j2
33 dest: "{{ llm_stack_config_dir }}/docker-compose.yml"
34 owner: "{{ runner_user }}"
35 group: "{{ runner_group }}"
36 mode: '0664'
37 notify: restart llm-stack
38
39- name: Create unified LLM stack environment file
40 template:
41 src: llm-stack.env.j2
42 dest: "{{ llm_stack_config_dir }}/.env"
43 owner: "{{ runner_user }}"
44 group: "{{ runner_group }}"
45 mode: '0664'
46 notify: restart llm-stack
47
48- name: Create LiteLLM environment file
49 template:
50 src: llm-stack-litellm.env.j2
51 dest: "{{ llm_stack_config_dir }}/litellm.env"
52 owner: "{{ runner_user }}"
53 group: "{{ runner_group }}"
54 mode: '0664'
55 notify: restart llm-stack
56
57- name: Create LiteLLM proxy configuration
58 template:
59 src: llm-stack-litellm-config.yml.j2
60 dest: "{{ llm_stack_config_dir }}/litellm-config.yaml"
61 owner: "{{ runner_user }}"
62 group: "{{ runner_group }}"
63 mode: '0664'
64 notify: restart llm-stack
65
66- name: Pull the pinned LLM stack images
67 community.docker.docker_image:
68 name: "{{ item }}"
69 source: pull
70 force_source: true
71 loop:
72 - "{{ container_images.ollama }}"
73 - "{{ container_images.litellm }}"
74 - "{{ container_images.openwebui }}"
75
76- name: Start unified LLM stack service
77 community.docker.docker_compose_v2:
78 project_src: "{{ llm_stack_config_dir }}"
79 state: present
80 register: llm_stack_start_result
81 check_mode: no
82
83- name: Wait for Ollama to be healthy
84 uri:
85 url: "http://localhost:{{ llm_stack_ollama_port }}/api/tags"
86 method: GET
87 status_code: 200
88 register: ollama_health
89 until: ollama_health.status == 200
90 retries: 30
91 delay: 10
92 when: llm_stack_start_result is changed
93 check_mode: no
94
95- name: Wait for OpenWebUI to be healthy
96 uri:
97 url: "http://localhost:{{ llm_stack_openwebui_port }}/api/health"
98 method: GET
99 status_code: 200
100 timeout: 30
101 register: openwebui_health
102 until: openwebui_health.status == 200
103 retries: 60
104 delay: 15
105 when: llm_stack_start_result is changed
106 check_mode: no
107
108- name: Wait for LiteLLM PostgreSQL to be healthy
109 wait_for:
110 host: localhost
111 port: "{{ llm_stack_litellm_port }}"
112 timeout: 30
113 when: llm_stack_start_result is changed
114 check_mode: no
115
116- name: Wait for LiteLLM to be healthy
117 uri:
118 url: "http://localhost:{{ llm_stack_litellm_port }}/"
119 method: GET
120 status_code: 200
121 register: litellm_health
122 until: litellm_health.status == 200
123 retries: 10
124 delay: 10
125 when: llm_stack_start_result is changed
126 check_mode: no
127
128- name: Pull declared Ollama models
129 when: llm_stack_pull_models
130 tags: [llm-models]
131 block:
132 - name: Query models already present in Ollama
133 uri:
134 url: "http://localhost:{{ llm_stack_ollama_port }}/api/tags"
135 method: GET
136 status_code: 200
137 register: ollama_present_models
138 check_mode: no
139
140 - name: Determine which declared models are missing
141 set_fact:
142 llm_stack_missing_models: >-
143 {{ llm_stack_models | map(attribute='name') | difference(
144 ollama_present_models.json.models | default([]) | map(attribute='name')
145 ) | list }}
146
147 - name: Report model pull plan
148 debug:
149 msg: >-
150 {{ (llm_stack_models | length) - (llm_stack_missing_models | length) }} of
151 {{ llm_stack_models | length }} declared models already present.
152 {{ 'Pulling: ' ~ (llm_stack_missing_models | join(', '))
153 if llm_stack_missing_models | length > 0
154 else 'Nothing to pull.' }}
155
156 # Pulls are serialised deliberately: concurrent pulls of multi-gigabyte models
157 # saturate both disk and the Ollama API, and a failure part-way is harder to
158 # attribute. Timeout is generous because a cold pull of a 14B model is slow.
159 - name: Pull missing Ollama models
160 uri:
161 url: "http://localhost:{{ llm_stack_ollama_port }}/api/pull"
162 method: POST
163 body_format: json
164 body:
165 model: "{{ item }}"
166 stream: false
167 status_code: 200
168 timeout: "{{ llm_stack_model_pull_timeout }}"
169 loop: "{{ llm_stack_missing_models }}"
170 loop_control:
171 label: "{{ item }}"
172 register: ollama_pull_result
173 # A model name that does not exist upstream returns 200 with an error body,
174 # so the status code alone is not a sufficient success signal.
175 failed_when: >-
176 ollama_pull_result.json.status | default('') != 'success'
177 check_mode: no
178
179- name: Display unified LLM stack deployment summary
180 debug:
181 msg: |
182 Unified LLM Stack Deployment:
183 - Status: {{ 'Started' if llm_stack_start_result is changed else 'Already running' }}
184
185 Ollama (Local LLM Server):
186 - Web UI: http://{{ ansible_default_ipv4.address }}:{{ llm_stack_ollama_port }}
187 - Configuration: {{ llm_stack_config_dir }}
188 - Data Storage: {{ llm_stack_ollama_data_dir }}
189 - GPU Support: {{ 'Enabled' if gpu_enabled else 'Disabled' }}
190 - Default Model: {{ llm_stack_ollama_default_model }}
191
192 OpenWebUI (Web Interface):
193 - Web UI: http://{{ ansible_default_ipv4.address }}:{{ llm_stack_openwebui_port }}
194 - Connected to Ollama: ollama:11434
195 - Name: {{ llm_stack_openwebui_name }}
196
197 LiteLLM (Unified Proxy):
198 - API Endpoint: http://{{ ansible_default_ipv4.address }}:{{ llm_stack_litellm_port }}
199 - Connected to Ollama: ollama:11434
200 - Available Models:
201 {% for model in llm_stack_models %}
202 - {{ model.name }} ({{ model.role }}): {{ model.description }}
203 {% endfor %}
204
205 Management Commands:
206 - cd {{ llm_stack_config_dir }} && docker compose logs -f
207 - cd {{ llm_stack_config_dir }} && docker compose restart
208 - cd {{ llm_stack_config_dir }} && docker compose ps