/
/
/
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: Start unified LLM stack service
67 community.docker.docker_compose_v2:
68 project_src: "{{ llm_stack_config_dir }}"
69 state: present
70 register: llm_stack_start_result
71 check_mode: no
72
73- name: Wait for Ollama to be healthy
74 uri:
75 url: "http://localhost:{{ llm_stack_ollama_port }}/api/tags"
76 method: GET
77 status_code: 200
78 register: ollama_health
79 until: ollama_health.status == 200
80 retries: 30
81 delay: 10
82 when: llm_stack_start_result is changed
83 check_mode: no
84
85- name: Wait for OpenWebUI to be healthy
86 uri:
87 url: "http://localhost:{{ llm_stack_openwebui_port }}/api/health"
88 method: GET
89 status_code: 200
90 timeout: 30
91 register: openwebui_health
92 until: openwebui_health.status == 200
93 retries: 60
94 delay: 15
95 when: llm_stack_start_result is changed
96 check_mode: no
97
98- name: Wait for LiteLLM PostgreSQL to be healthy
99 wait_for:
100 host: localhost
101 port: "{{ llm_stack_litellm_port }}"
102 timeout: 30
103 when: llm_stack_start_result is changed
104 check_mode: no
105
106- name: Wait for LiteLLM to be healthy
107 uri:
108 url: "http://localhost:{{ llm_stack_litellm_port }}/"
109 method: GET
110 status_code: 200
111 register: litellm_health
112 until: litellm_health.status == 200
113 retries: 10
114 delay: 10
115 when: llm_stack_start_result is changed
116 check_mode: no
117
118- name: Pull declared Ollama models
119 when: llm_stack_pull_models
120 tags: [llm-models]
121 block:
122 - name: Query models already present in Ollama
123 uri:
124 url: "http://localhost:{{ llm_stack_ollama_port }}/api/tags"
125 method: GET
126 status_code: 200
127 register: ollama_present_models
128 check_mode: no
129
130 - name: Determine which declared models are missing
131 set_fact:
132 llm_stack_missing_models: >-
133 {{ llm_stack_models | map(attribute='name') | difference(
134 ollama_present_models.json.models | default([]) | map(attribute='name')
135 ) | list }}
136
137 - name: Report model pull plan
138 debug:
139 msg: >-
140 {{ (llm_stack_models | length) - (llm_stack_missing_models | length) }} of
141 {{ llm_stack_models | length }} declared models already present.
142 {{ 'Pulling: ' ~ (llm_stack_missing_models | join(', '))
143 if llm_stack_missing_models | length > 0
144 else 'Nothing to pull.' }}
145
146 # Pulls are serialised deliberately: concurrent pulls of multi-gigabyte models
147 # saturate both disk and the Ollama API, and a failure part-way is harder to
148 # attribute. Timeout is generous because a cold pull of a 14B model is slow.
149 - name: Pull missing Ollama models
150 uri:
151 url: "http://localhost:{{ llm_stack_ollama_port }}/api/pull"
152 method: POST
153 body_format: json
154 body:
155 model: "{{ item }}"
156 stream: false
157 status_code: 200
158 timeout: "{{ llm_stack_model_pull_timeout }}"
159 loop: "{{ llm_stack_missing_models }}"
160 loop_control:
161 label: "{{ item }}"
162 register: ollama_pull_result
163 # A model name that does not exist upstream returns 200 with an error body,
164 # so the status code alone is not a sufficient success signal.
165 failed_when: >-
166 ollama_pull_result.json.status | default('') != 'success'
167 check_mode: no
168
169- name: Display unified LLM stack deployment summary
170 debug:
171 msg: |
172 Unified LLM Stack Deployment:
173 - Status: {{ 'Started' if llm_stack_start_result is changed else 'Already running' }}
174
175 Ollama (Local LLM Server):
176 - Web UI: http://{{ ansible_default_ipv4.address }}:{{ llm_stack_ollama_port }}
177 - Configuration: {{ llm_stack_config_dir }}
178 - Data Storage: {{ llm_stack_ollama_data_dir }}
179 - GPU Support: {{ 'Enabled' if gpu_enabled else 'Disabled' }}
180 - Default Model: {{ llm_stack_ollama_default_model }}
181
182 OpenWebUI (Web Interface):
183 - Web UI: http://{{ ansible_default_ipv4.address }}:{{ llm_stack_openwebui_port }}
184 - Connected to Ollama: ollama:11434
185 - Name: {{ llm_stack_openwebui_name }}
186
187 LiteLLM (Unified Proxy):
188 - API Endpoint: http://{{ ansible_default_ipv4.address }}:{{ llm_stack_litellm_port }}
189 - Connected to Ollama: ollama:11434
190 - Available Models:
191 {% for model in llm_stack_models %}
192 - {{ model.name }} ({{ model.role }}): {{ model.description }}
193 {% endfor %}
194
195 Management Commands:
196 - cd {{ llm_stack_config_dir }} && docker compose logs -f
197 - cd {{ llm_stack_config_dir }} && docker compose restart
198 - cd {{ llm_stack_config_dir }} && docker compose ps