Compare commits

...

19 Commits

Author SHA1 Message Date
KevinMidboe 871b8d1f83 moved cache & db host vars to group 2026-08-06 00:42:45 +02:00
KevinMidboe c349880bf5 configure docker network separately 2026-08-06 00:38:35 +02:00
KevinMidboe c18487ac23 pasre WEB_IPS as json 2026-08-05 23:21:16 +02:00
KevinMidboe d9d1f6ca0b moved umami vars to group 2026-08-05 22:40:07 +02:00
KevinMidboe 8aae1a9c1f refresh apt before installing docker 2026-08-05 22:37:44 +02:00
KevinMidboe 2d168877c8 added .DS_Store to gitignore 2026-08-05 20:55:47 +02:00
KevinMidboe 0b3bb1e3f4 better handling when parsing env into dict 2026-08-05 20:55:16 +02:00
KevinMidboe 5d01d6555b remove whitelisting of hosts in prome-grafana play 2026-08-05 20:54:50 +02:00
KevinMidboe b9b02edf3e remove whitelisting of hosts in elasticsearch play 2026-08-05 20:54:21 +02:00
KevinMidboe a861de3392 always install cadvisor_exporter on workers 2026-08-05 20:53:59 +02:00
KevinMidboe 4c4ab3184f only allow port 80 on web 2026-08-05 20:53:36 +02:00
KevinMidboe 094d02ff7f umami analytics play & role 2026-08-05 20:53:08 +02:00
KevinMidboe c89e8c565a play for installing traefik on lb servers 2026-08-05 20:50:58 +02:00
KevinMidboe ecdf64ffac only add motd description when defined 2026-08-05 20:50:35 +02:00
KevinMidboe 524e0d44f7 update defaults variables 2026-08-05 20:49:57 +02:00
KevinMidboe 6ce2d9d3bd create separate role for setting up unix users 2026-08-05 20:49:26 +02:00
KevinMidboe 1fc2506cf4 update docker role & add separate play 2026-08-05 20:48:24 +02:00
KevinMidboe 7013159c29 removed tailscale play, we do this in pulumi instaed 2026-08-05 20:48:06 +02:00
KevinMidboe d5450fd311 upgrade ELK stack to 9.4.4
Inclues apps: elasticsearch, logstash, kibana
2026-07-27 18:48:48 +02:00
52 changed files with 830 additions and 151 deletions
+1
View File
@@ -1 +1,2 @@
.env
.DS_Store
+15 -4
View File
@@ -1,10 +1,7 @@
---
ansible_python_interpreter: /usr/bin/python3
install_cadvisor_exporter: false
install_nginx_exporter: false
install_redis_exporter: false
users:
- username: forge
- username: kevin
oh_my_zsh:
theme: robbyrussell
- username: root
@@ -13,3 +10,17 @@ users:
custom_firewall_ports:
- 9100 # node exporter
- 9191 # fail2ban exporter
# exporter versions
node_exporter_version: "1.9.0"
fail2ban_exporter_version: "0.10.2"
cadvisor_version: "0.46.0"
#redis_exporter_version: ??
mysqld_exporter_version: "0.17.2"
nginx_exporter_version: "1.4.1"
# disable exporters by default
install_node_exporter: true
install_fail2ban_exporter: true
install_cadvisor_exporter: false
install_redis_exporter: false
install_mysqld_exporter: false
install_nginx_exporter: false
+21 -2
View File
@@ -1,6 +1,25 @@
---
## REQUIRED
# DOMAIN ""
# ANALYTICS_IP ""
# WEB_IPS '["", ""]'
# apt_packages:
# - nginx
custom_firewall_ports:
- 80
- 443
install_nginx_exporter: true
description: "load balancer serving web servers for retailor.io"
# - 9100 # traefik exporter - internal only
# - 8080 # traefik dashboard - internal only
# install_nginx_exporter: true
description: "load balancer serving traefik webserver for retailor.io"
traefik_image: "traefik:v3.1"
traefik_acme_email: webmaster@retailor.io
traefik_config_dir: "/etc/traefik"
traefik_dynamic_dir: "/etc/traefik/dynamic"
traefik_web_port: 80
traefik_websecure_port: 443
traefik_dashboard_port: 8080
traefik_dashboard_insecure: true
+37
View File
@@ -0,0 +1,37 @@
---
# ---------------------------------------------------------------------------
# Global variables for the docker + umami deployment
# ---------------------------------------------------------------------------
custom_firewall_ports:
- 3000
description: Umami web analytics
# --- Docker role ---
docker_users_to_add:
- "{{ ansible_user }}" # add the SSH user to the docker group
# --- Umami role ---
umami_base_dir: /opt/umami
umami_image: "ghcr.io/umami-software/umami:latest"
umami_container_name: umami
umami_port: 3000 # host port Umami will be exposed on
# Address the umami_port is bound to on the host.
# - "127.0.0.1" if your reverse proxy runs on this same host (loopback only)
# - "0.0.0.0" if your reverse proxy is elsewhere (another host/container) and
# needs to reach this port over the network
umami_bind_address: "0.0.0.0"
# Postgres settings (used by the umami-db container)
umami_db_container_name: umami-db
umami_db_image: "postgres:15-alpine"
umami_db_name: umami
umami_db_user: umami
# IMPORTANT: override this in inventory/vars, --extra-vars, or (better) Ansible Vault.
# Do not leave the default password in production.
umami_db_password: "fc7tBTxsuAulmYmSl8AE0q13eq3f94f7u3u6"
# A random string used by Umami to encrypt/salt data. Generate your own, e.g.:
# openssl rand -hex 32
# Override this in production via --extra-vars or vault.
umami_app_secret: "dichotomizing-triads-cosmochemistry-amidols"
+1 -2
View File
@@ -1,8 +1,7 @@
---
custom_firewall_ports:
- 80
- 443
apt_packages:
- nfs-common
install_nginx_exporter: true
install_cadvisor_exporter: true
description: 'webserver node for hosting retailor laravel project'
+1
View File
@@ -1,4 +1,5 @@
---
apt_packages:
- nfs-common
install_cadvisor_exporter: true
description: 'queue server for laravel'
+1
View File
@@ -5,5 +5,6 @@
- role: roles/base-packages # - Basic server setup and configuration
- role: roles/fail2ban # - SSH Brute force protection
- role: roles/firewall # - Firewall, if firewall_enable is true
- role: roles/users
- role: roles/oh-my-zsh
- role: roles/motd
+5
View File
@@ -0,0 +1,5 @@
---
- name: Docker on host
hosts: all
roles:
- role: roles/docker
+3 -3
View File
@@ -1,12 +1,12 @@
---
- name: Install ELK stack using Docker
hosts: elasticsearch
hosts: all
vars:
elk_version: "8.17.0"
elk_version: "9.4.4"
roles:
# TODO check for docker install before purging it. This nukes containers
# we might not want destroyed.
# Leave a file, or read the same input requirements into a apt check
# which skips purge if satisfied.
# - role: roles/docker
- role: roles/docker
- role: roles/elasticsearch
+2 -2
View File
@@ -1,10 +1,10 @@
---
- name: Install prometheus & grafana using Docker
hosts: grafana
hosts: all
vars:
prometheus_version: "latest"
grafana_version: "latest"
cadvisor_version: "0.46.0"
roles:
# - role: roles/docker
- role: roles/docker
- role: roles/prometheus-grafana
-7
View File
@@ -1,7 +0,0 @@
---
- name: Install tailscale
hosts: all
roles:
- role: roles/tailscale
vars:
tailscale_authkey: "{{ lookup('env', 'TAILSCALE_KEY') }}"
+8
View File
@@ -0,0 +1,8 @@
---
- name: Install traefik using Docker
hosts: all
roles:
- role: roles/firewall
- role: roles/docker
- role: roles/env
- role: roles/traefik
+10
View File
@@ -0,0 +1,10 @@
---
- name: Install umami using Docker
hosts: all
vars:
umami_version: "9.4.4"
roles:
- role: roles/firewall
- role: roles/docker
- role: roles/env
- role: roles/umami
+76
View File
@@ -0,0 +1,76 @@
# docker
Installs Docker CE from the official Docker apt repository on Ubuntu/Debian.
## What changed from the original role
The original role had several bugs that would have made it fail or behave
unpredictably:
- **`tasks/main.yml` was dead code that duplicated `install.yml` incorrectly.**
Nothing ever included it (only `main-distro-check.yml` was wired up), and
it referenced undefined variables (`install_packages`,
`clean_install_remove_packages` had no `become`, etc). It's been replaced
by a single real entrypoint.
- **`install.yml` added the apt repo but never fetched the GPG key**, so
`apt-get update` would fail signature verification. Key download and repo
registration are now in one idempotent flow using `get_url` +
`apt_repository` instead of raw `shell` commands.
- **`check_distro.yml` used `meta: end_play`**, which ends the *entire
Ansible run* for *all* hosts in the play, not just this role/host. This is
almost never what you want in a multi-host play. It's replaced with a
`docker_distro_supported` fact that gates subsequent tasks, with an
optional hard `fail` (`docker_fail_on_unsupported_distro`, default `true`).
- **`service.yml` had the exact same task listed twice** (one with a `when`
guard, one without). Deduplicated into a single task.
- **Package state `latest`** was used for prerequisites and Docker itself,
which causes unnecessary upgrades (and potential breakage) on every run.
Changed to `present`; use `docker_install_packages` + your own
update strategy if you want upgrades.
- **No handler was ever notified.** Installing/upgrading Docker packages now
notifies the `Restart Docker` handler.
- **`docker` group creation used the `user` module** to create a "user" named
docker, which is not the same as a group and wasn't added to any actual
user. Replaced with a proper `group` task plus a `docker_users` list you
populate to add real users to the group.
- All tasks now use fully-qualified module names (`ansible.builtin.*`) and
`name:`/`state:` keyword syntax instead of the deprecated
`key=value` shorthand.
## Role variables
See `defaults/main.yml` for the full list. Commonly overridden:
```yaml
docker_users:
- deploy
- ci
docker_install_packages:
- docker-ce
- docker-ce-cli
- containerd.io
- docker-buildx-plugin
- docker-compose-plugin
docker_fail_on_unsupported_distro: true
```
## Example playbook
```yaml
- hosts: docker_hosts
become: true
roles:
- role: docker
vars:
docker_users:
- "{{ ansible_user }}"
```
## Tags
- `docker_check_distro`
- `docker_install`
- `docker_users`
- `docker_service`
+32 -3
View File
@@ -1,4 +1,12 @@
clean_install_remove_packages:
---
# Distros this role knows how to configure the Docker apt repo for.
docker_supported_distros:
- ubuntu
- debian
# Packages to purge before installing Docker CE, to avoid conflicts
# with distro-provided / older Docker packages.
docker_clean_install_remove_packages:
- docker.io
- docker-doc
- docker-compose
@@ -6,10 +14,31 @@ clean_install_remove_packages:
- containerd
- runc
install_packages:
# Packages installed from the official Docker apt repository.
docker_install_packages:
- docker-ce
- docker-ce-cli
- containerd.io
- docker-buildx-plugin
- docker-compose
- docker-compose-plugin
# Base URL for the Docker apt repository. {{ '%s' }} is filled in with the
# lowercased distro name (ubuntu/debian).
docker_apt_repo_url: "https://download.docker.com/linux"
# Where the Docker GPG signing key is stored.
docker_apt_keyring_dir: /etc/apt/keyrings
docker_apt_keyring_file: "{{ docker_apt_keyring_dir }}/docker.asc"
# apt cache freshness, in seconds, before Ansible refreshes it.
docker_apt_cache_valid_time: 3600
# Users to add to the "docker" group (passwordless docker CLI access).
docker_users: []
# Whether to start and enable the docker service.
docker_service_enabled: true
docker_service_state: started
# Fail the play if the distro isn't supported, instead of silently skipping.
docker_fail_on_unsupported_distro: true
+1 -1
View File
@@ -1,5 +1,5 @@
---
- name: Restart Docker
systemd:
ansible.builtin.systemd:
name: docker
state: restarted
+12 -3
View File
@@ -1,13 +1,22 @@
---
galaxy_info:
role_name: docker
author: Your Name
description: Ansible role to install and manage Docker on Debian
description: Installs and manages Docker CE from the official Docker apt repository
license: MIT
min_ansible_version: "2.9"
min_ansible_version: "2.14"
platforms:
- name: Ubuntu
versions:
- focal
- jammy
- noble
- name: Debian
versions:
- all
- bullseye
- bookworm
galaxy_tags:
- docker
- containers
dependencies: []
+19 -24
View File
@@ -1,27 +1,22 @@
---
- name: Check if the current distro is supported (Ubuntu or Debian)
set_fact:
distro_supported: "{{ ansible_facts['distribution'].lower() in supported_distros }}"
tags:
- check_distro
- name: Determine if the current distro is supported
ansible.builtin.set_fact:
docker_distro_supported: "{{ ansible_facts['distribution'] | lower in docker_supported_distros }}"
- name: Set installation URL based on the distro
set_fact:
install_url: "https://download.docker.com/linux/{{ ansible_facts['distribution'].lower() }} {{ ansible_distribution_release }} stable"
when: distro_supported
tags:
- set_url
- name: Log Unsupported Distro
debug:
msg: "The {{ ansible_facts['distribution'] }} distribution is not supported. Skipping Docker installation."
when: not distro_supported
tags:
- unsupported_distro
- name: Skip Docker installation task if distro is unsupported
meta: end_play
when: not distro_supported
tags:
- end_play
- name: Fail on unsupported distro
ansible.builtin.fail:
msg: >-
{{ ansible_facts['distribution'] }} is not a supported distribution for
the docker role. Supported: {{ docker_supported_distros | join(', ') }}.
when:
- not docker_distro_supported
- docker_fail_on_unsupported_distro
- name: Notify unsupported distro is being skipped
ansible.builtin.debug:
msg: >-
{{ ansible_facts['distribution'] }} is not supported by the docker role.
Skipping Docker installation on this host.
when:
- not docker_distro_supported
- not docker_fail_on_unsupported_distro
+48 -9
View File
@@ -1,12 +1,51 @@
---
- name: Add Docker repository
apt_repository:
repo: deb {{ install_url }}
state: present
when: distro_supported
- name: Remove conflicting/legacy Docker packages
ansible.builtin.package:
name: "{{ docker_clean_install_remove_packages }}"
state: absent
- name: Install Docker
apt:
name: "{{ docker_package }}"
- name: Install prerequisite packages
ansible.builtin.package:
name:
- ca-certificates
- curl
- gnupg
state: present
when: distro_supported
update_cache: true
- name: Ensure apt keyrings directory exists
ansible.builtin.file:
path: "{{ docker_apt_keyring_dir }}"
state: directory
mode: "0755"
- name: Download Docker's official GPG key
ansible.builtin.get_url:
url: "{{ docker_apt_repo_url }}/{{ ansible_facts['distribution'] | lower }}/gpg"
dest: "{{ docker_apt_keyring_file }}"
mode: "0644"
force: true
- name: Add Docker apt repository
ansible.builtin.apt_repository:
repo: >-
deb [arch={{ 'amd64' if ansible_facts['architecture'] == 'x86_64' else ansible_facts['architecture'] }}
signed-by={{ docker_apt_keyring_file }}]
{{ docker_apt_repo_url }}/{{ ansible_facts['distribution'] | lower }}
{{ ansible_facts['distribution_release'] }} stable
state: present
filename: docker
update_cache: false
- name: Update apt cache
ansible.builtin.apt:
update_cache: true
cache_valid_time: "{{ docker_apt_cache_valid_time }}"
- name: Install Docker packages
ansible.builtin.apt:
pkg: "{{ item }}"
state: present
update_cache: yes
with_items: "{{ docker_install_packages | default([]) }}"
notify: Restart Docker
-11
View File
@@ -1,11 +0,0 @@
---
- name: Include distro check tasks
include_tasks: check_distro.yml
tags:
- check_distro
- name: Include Docker installation tasks if distro is supported
include_tasks: install.yml
when: distro_supported
tags:
- install
+19 -41
View File
@@ -1,45 +1,23 @@
---
- name: Clean install by removing any docker package
package: name={{ item }} state=absent
with_items: "{{ clean_install_remove_packages }}"
- name: Assert supported distribution
ansible.builtin.include_tasks: check_distro.yml
tags:
- docker_check_distro
- name: Ensure curl & ca-certs are installed
package:
name:
- ca-certificates
- curl
- gnupg
state: latest
- name: Install Docker
ansible.builtin.include_tasks: install.yml
when: docker_distro_supported
tags:
- docker_install
- name: Ensure docker keyring file exists
file:
path: /etc/apt/keyrings/docker.gpg
state: touch
- name: Download docker gpg key and add to keyrings
shell: |
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor --yes -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg
- name: Configure Docker group membership
ansible.builtin.include_tasks: users.yml
when: docker_distro_supported and docker_users | length > 0
tags:
- docker_users
- name: Sign and add docker deb source
shell: |
echo \
"deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
"$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | \
tee /etc/apt/sources.list.d/docker.list > /dev/null
- name: Update apt sources
become: true
apt:
update_cache: yes
cache_valid_time: 1
- name: Install docker packages
package: name={{ item }} state=latest
with_items: "{{ install_packages }}"
- name: Ensure group docker exists
user:
name: docker
state: present
- name: Manage Docker service
ansible.builtin.include_tasks: service.yml
when: docker_distro_supported
tags:
- docker_service
+3 -11
View File
@@ -1,14 +1,6 @@
---
- name: Start and enable Docker service
systemd:
ansible.builtin.systemd:
name: docker
enabled: yes
state: started
- name: Start and enable Docker service
systemd:
name: docker
enabled: yes
state: started
when: distro_supported
enabled: "{{ docker_service_enabled }}"
state: "{{ docker_service_state }}"
+12
View File
@@ -0,0 +1,12 @@
---
- name: Ensure docker group exists
ansible.builtin.group:
name: docker
state: present
- name: Add users to the docker group
ansible.builtin.user:
name: "{{ item }}"
groups: docker
append: true
loop: "{{ docker_users }}"
@@ -1,9 +1,4 @@
---
- name: Create a Docker network for Elasticsearch
docker_network:
name: elk_network
state: present
- name: Pull Elasticsearch Docker image
docker_image:
name: docker.elastic.co/elasticsearch/elasticsearch-wolfi:{{ elk_version }}
+11
View File
@@ -0,0 +1,11 @@
---
- name: Create a Docker network for Elasticsearch
docker_network:
name: elk_network
state: present
driver: bridge
ipam_config:
- subnet: "172.19.0.0/16"
gateway: "172.19.0.1"
iprange: "172.19.0.0/24"
+1
View File
@@ -2,6 +2,7 @@
# ensure we have variables from .env files
- include_tasks: ../roles/env/tasks/main.yml
- include_tasks: elk-network.yml
- include_tasks: elasticsearch.yml
- include_tasks: kibana.yml
- include_tasks: logstash.yml
+7 -3
View File
@@ -1,12 +1,16 @@
---
- name: Read .env file
ansible.builtin.slurp:
src: "../.env"
src: "{{ playbook_dir }}/../.env"
delegate_to: localhost
register: env_file
- name: Parse .env file
- name: Build list of key/value pairs
ansible.builtin.set_fact:
env_vars: "{{ dict(env_file['content'] | b64decode | split('\n') | select('search', '=') | map('split', '=', 1) | list) }}"
env_lines: "{{ (env_file['content'] | b64decode).splitlines() | select('match', '^[^#=]+=.*$') | list }}"
- name: Parse .env file into a dict
ansible.builtin.set_fact:
env_vars: "{{ env_vars | default({}) | combine({ (item.split('=', 1)[0] | trim): (item.split('=', 1)[1] | trim) }) }}"
loop: "{{ env_lines }}"
+1 -1
View File
@@ -33,7 +33,7 @@
owner: root
group: root
mode: +x
when: description
when: description is defined and description | length > 0
- name: check if help-text motd exists
stat:
+1 -6
View File
@@ -1,9 +1,4 @@
---
- name: Create a Docker network for Grafana
docker_network:
name: monitoring_network
state: present
- name: Create grafana datasources directory on host
file:
path: /etc/grafana/datasources
@@ -87,7 +82,7 @@
- /etc/grafana/datasources:/etc/grafana/provisioning/datasources
- /etc/grafana/dashboards:/etc/grafana/provisioning/dashboards
networks:
- name: monitoring_network
- name: monitoring
env:
GF_SECURITY_ADMIN_PASSWORD: "{{ env_vars.GRAFANA_PASSWORD }}" # Customize the password
+1 -6
View File
@@ -1,9 +1,4 @@
---
- name: Create a Docker network for loki
docker_network:
name: monitoring_network
state: present
- name: Pull loki Docker image
docker_image:
name: grafana/loki:latest
@@ -33,4 +28,4 @@
volumes:
- /etc/loki/config.yml:/etc/loki/config.yml
networks:
- name: monitoring_network
- name: monitoring
+1
View File
@@ -2,6 +2,7 @@
# ensure we have variables from .env files
- include_tasks: ../roles/env/tasks/main.yml
- include_tasks: monitoring-network.yml
- include_tasks: prometheus.yml
- include_tasks: grafana.yml
@@ -0,0 +1,5 @@
---
- name: Create a Docker network for Monitoring
docker_network:
name: monitoring
state: present
@@ -1,9 +1,4 @@
---
- name: Create a Docker network for Prometheus
docker_network:
name: monitoring_network
state: present
- name: Pull Prometheus Docker image
docker_image:
name: prom/prometheus
@@ -32,5 +27,5 @@
volumes:
- /etc/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
networks:
- name: monitoring_network
- name: monitoring
Submodule roles/tailscale deleted from d225db4915
+14
View File
@@ -0,0 +1,14 @@
traefik_config_dir: "/etc/traefik"
traefik_dynamic_dir: "/etc/traefik/dynamic"
traefik_web_port: 80
traefik_websecure_port: 443
traefik_dashboard_port: 8080
traefik_metrics_port: 8100
traefik_dashboard_insecure: true # ufw should only allow from tailscale or vnet
domain: "{{ lookup('env', 'DOMAIN') | default('', true) }}"
# Single source of truth for static routing. Expected to be overridden
# from group_vars, -e, or an env-var lookup at the play level — see
# playbook.yml for the TRAEFIK_ROUTES example.
traefik_web_private_ips: "{{ lookup('env', 'WEB_IPS') | default('[]', true) | from_json }}"
traefik_analytics_private_ip: "{{ lookup('env', 'ANALYTICS_IP') | default('', true) }}"
+9
View File
@@ -0,0 +1,9 @@
---
- name: Reload systemd
ansible.builtin.systemd:
daemon_reload: true
- name: Restart traefik
ansible.builtin.systemd:
name: traefik
state: restarted
+77
View File
@@ -0,0 +1,77 @@
---
- name: Fail fast if no routes were supplied
ansible.builtin.fail:
msg: >-
traefik_web_private_ips is empty. Set the WEB_IPS env var
(JSON list) before running, or override the variable directly, e.g.
-e traefik_web_private_ips='["10.0.1.11","10.0.1.12"]'
when: env_vars.WEB_IPS | length == 0
- name: Ensure Traefik config directories exist
ansible.builtin.file:
path: "{{ item }}"
state: directory
mode: "0755"
loop:
- "{{ traefik_config_dir }}"
- "{{ traefik_dynamic_dir }}"
- name: Create acme.json with correct permissions
ansible.builtin.file:
path: "{{ traefik_config_dir }}/acme.json"
state: touch
owner: root
group: root
mode: "0600"
modification_time: preserve
access_time: preserve
- name: Deploy Traefik static config
ansible.builtin.template:
src: traefik.yml.j2
dest: "{{ traefik_config_dir }}/traefik.yml"
mode: "0644"
notify: Restart traefik
- name: Deploy dynamic downstream routes file
ansible.builtin.template:
src: dynamic-hosts.yml.j2
dest: "{{ traefik_dynamic_dir }}/hosts.yml"
mode: "0644"
notify: Restart traefik
- name: Deploy dynamic analytics file
ansible.builtin.template:
src: dynamic-analytics.yml.j2
dest: "{{ traefik_dynamic_dir }}/analytics.yml"
mode: "0644"
notify: Restart traefik
- name: Pull traefik image
community.docker.docker_image:
name: "{{ traefik_image }}"
source: pull
notify: Restart traefik
- name: Deploy traefik systemd unit
ansible.builtin.template:
src: traefik.service.j2
dest: /etc/systemd/system/traefik.service
owner: root
group: root
mode: "0644"
notify: Reload systemd
- name: Enable and start traefik service
ansible.builtin.systemd:
name: traefik
enabled: true
state: started
daemon_reload: true
- name: Remove dangling/old traefik images
community.docker.docker_prune:
images: true
images_filters:
dangling: true
+1
View File
@@ -0,0 +1 @@
- include_tasks: install.yml
@@ -0,0 +1,21 @@
## Managed by Ansible — do not edit by hand.
## Source: traefik_analytics_private_ip (group_vars/lb.yml)
http:
routers:
analytics:
rule: "Host(`analytics.{{ env_vars.DOMAIN }}`)"
entryPoints:
- websecure
service: analytics-svc
tls:
certResolver: letsencrypt
services:
analytics-svc:
loadBalancer:
servers:
- url: "http://{{ env_vars.ANALYTICS_IP }}:3000"
healthCheck:
path: /api/heartbeat
interval: "10s"
timeout: "3s"
@@ -0,0 +1,20 @@
## Managed by Ansible — do not edit by hand.
## Source: traefik_web_private_ips (group_vars/lb.yml)
http:
routers:
retailor-web:
rule: "Host(`app.{{ env_vars.DOMAIN }}`)"
service: retailor-web-svc
entryPoints:
- websecure
tls:
certResolver: letsencrypt
services:
retailor-web-svc:
loadBalancer:
servers:
{% for web_ip in env_vars.WEB_IPS | from_json %}
- url: "http://{{ web_ip }}:80"
{% endfor %}
@@ -0,0 +1,28 @@
[Unit]
Description=Traefik reverse proxy
After=docker.service network-online.target
Requires=docker.service
Wants=network-online.target
[Service]
TimeoutStartSec=0
Restart=always
RestartSec=5
# Clean up any stale container from a previous run/crash
ExecStartPre=-/usr/bin/docker rm -f traefik
ExecStart=/usr/bin/docker run --rm \
--name traefik \
--network host \
--security-opt no-new-privileges:true \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
-v {{ traefik_config_dir }}/traefik.yml:/etc/traefik/traefik.yml:ro \
-v {{ traefik_dynamic_dir }}:/etc/traefik/dynamic:ro \
-v {{ traefik_config_dir }}/acme.json:/etc/traefik/acme.json \
{{ traefik_image }}
ExecStop=/usr/bin/docker stop traefik
[Install]
WantedBy=multi-user.target
+38
View File
@@ -0,0 +1,38 @@
entryPoints:
web:
address: ":{{ traefik_web_port }}"
websecure:
address: ":{{ traefik_websecure_port }}"
traefik:
address: ":{{ traefik_dashboard_port }}"
metrics:
address: ":{{ traefik_metrics_port }}"
providers:
file:
directory: "{{ traefik_dynamic_dir }}"
watch: true
api:
dashboard: true
insecure: {{ traefik_dashboard_insecure | lower }}
log:
level: INFO
accessLog: {}
certificatesResolvers:
letsencrypt:
acme:
email: "{{ traefik_acme_email }}"
storage: /etc/traefik/acme.json
httpChallenge:
entryPoint: web
metrics:
prometheus:
entryPoint: metrics
addEntryPointsLabels: true
addServicesLabels: true
addRoutersLabels: true
+7
View File
@@ -0,0 +1,7 @@
---
umami_python_docker_sdk_packages:
- python3-pip
- python3-docker
umami_restart_policy: unless-stopped
umami_docker_network: umami_net
+11
View File
@@ -0,0 +1,11 @@
---
docker_apt_arch: "{{ 'amd64' if ansible_architecture == 'x86_64' else 'arm64' }}"
docker_packages:
- docker-ce
- docker-ce-cli
- containerd.io
- docker-buildx-plugin
- docker-compose-plugin
docker_service_state: started
docker_service_enabled: true
+6
View File
@@ -0,0 +1,6 @@
---
- name: restart docker
ansible.builtin.systemd:
name: docker
state: restarted
daemon_reload: true
+98
View File
@@ -0,0 +1,98 @@
---
- name: Ensure required system packages are present
ansible.builtin.apt:
name:
- ca-certificates
- curl
- gnupg
- lsb-release
state: present
update_cache: true
cache_valid_time: 3600
- name: Ensure /etc/apt/keyrings directory exists
ansible.builtin.file:
path: /etc/apt/keyrings
state: directory
mode: "0755"
- name: Check if Docker GPG key already present
ansible.builtin.stat:
path: /etc/apt/keyrings/docker.asc
register: docker_gpg_key
- name: Download Docker's official GPG key
ansible.builtin.get_url:
url: https://download.docker.com/linux/ubuntu/gpg
dest: /etc/apt/keyrings/docker.asc
mode: "0644"
when: not docker_gpg_key.stat.exists
- name: Add Docker apt repository (deb822 format)
ansible.builtin.deb822_repository:
name: docker
types: [deb]
uris: "https://download.docker.com/linux/ubuntu"
suites: "{{ ansible_distribution_release }}"
components: [stable]
architectures: "{{ docker_apt_arch }}"
signed_by: /etc/apt/keyrings/docker.asc
state: present
register: docker_repo_added
- name: Update apt cache after adding Docker repo
ansible.builtin.apt:
update_cache: true
when: docker_repo_added.changed
- name: Install Docker Engine and plugins
ansible.builtin.apt:
name: "{{ docker_packages }}"
state: present
notify: restart docker
- name: Ensure docker service is started and enabled
ansible.builtin.systemd:
name: docker
state: "{{ docker_service_state }}"
enabled: "{{ docker_service_enabled }}"
- name: Ensure docker group exists
ansible.builtin.group:
name: docker
state: present
- name: Add user(s) to the docker group
ansible.builtin.user:
name: "{{ item }}"
groups: docker
append: true
loop: "{{ docker_users_to_add }}"
register: docker_group_added
- name: Notify about needing to re-login for group changes
ansible.builtin.debug:
msg: >-
User '{{ item.item }}' was added to the docker group. They will need to
log out and back in (or start a new SSH session) for this to take effect
without using sudo for docker commands.
loop: "{{ docker_group_added.results }}"
when: item.changed
- name: Verify Docker is installed and working
ansible.builtin.command: docker --version
register: docker_version_output
changed_when: false
- name: Show installed Docker version
ansible.builtin.debug:
msg: "{{ docker_version_output.stdout }}"
- name: Verify Docker Compose plugin is installed
ansible.builtin.command: docker compose version
register: docker_compose_version_output
changed_when: false
- name: Show installed Docker Compose version
ansible.builtin.debug:
msg: "{{ docker_compose_version_output.stdout }}"
+7
View File
@@ -0,0 +1,7 @@
---
- name: recreate umami stack
community.docker.docker_compose_v2:
project_src: "{{ umami_base_dir }}"
state: present
recreate: always
listen: recreate umami stack
+50
View File
@@ -0,0 +1,50 @@
---
- name: Ensure pip and Docker SDK for Python are installed (needed by community.docker modules)
ansible.builtin.apt:
name: "{{ umami_python_docker_sdk_packages }}"
state: present
update_cache: true
cache_valid_time: 3600
- name: Ensure Umami base directory exists
ansible.builtin.file:
path: "{{ umami_base_dir }}"
state: directory
mode: "0750"
owner: root
group: root
- name: Deploy docker-compose.yml for Umami stack
ansible.builtin.template:
src: docker-compose.yml.j2
dest: "{{ umami_base_dir }}/docker-compose.yml"
mode: "0640"
owner: root
group: root
notify: recreate umami stack
- name: Flush handlers so compose file changes apply before we verify status
ansible.builtin.meta: flush_handlers
- name: Start (or ensure running) the Umami + Postgres stack
community.docker.docker_compose_v2:
project_src: "{{ umami_base_dir }}"
state: present
register: umami_compose_result
- name: Show compose deployment result
ansible.builtin.debug:
var: umami_compose_result.changed
- name: Wait for Umami HTTP endpoint to become available
ansible.builtin.uri:
url: "http://127.0.0.1:{{ umami_port }}/api/heartbeat"
status_code: 200
register: umami_health
until: umami_health.status == 200
retries: 5
delay: 10
- name: Umami health check result
ansible.builtin.debug:
msg: "Umami is up and responding on port {{ umami_port }}."
@@ -0,0 +1,40 @@
{{ ansible_managed | comment }}
# Based on: https://github.com/umami-software/umami/blob/master/docker-compose.yml
services:
umami:
image: "{{ umami_image }}"
container_name: "{{ umami_container_name }}"
ports:
- "{{ umami_bind_address }}:{{ umami_port }}:3000"
environment:
DATABASE_URL: "postgresql://{{ umami_db_user }}:{{ umami_db_password }}@db:5432/{{ umami_db_name }}"
APP_SECRET: "{{ umami_app_secret }}"
depends_on:
db:
condition: service_healthy
init: true
restart: always
healthcheck:
test: ["CMD-SHELL", "curl http://localhost:3000/api/heartbeat"]
interval: 5s
timeout: 5s
retries: 5
db:
image: "{{ umami_db_image }}"
container_name: "{{ umami_db_container_name }}"
environment:
POSTGRES_DB: "{{ umami_db_name }}"
POSTGRES_USER: "{{ umami_db_user }}"
POSTGRES_PASSWORD: "{{ umami_db_password }}"
volumes:
- umami-db-data:/var/lib/postgresql/data
restart: always
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 5
volumes:
umami-db-data:
+47
View File
@@ -0,0 +1,47 @@
---
- name: Ensure required packages for user management are present
ansible.builtin.apt:
name:
- sudo
- acl
state: present
update_cache: true
become: true
- name: Create primary group for each user
ansible.builtin.group:
name: "{{ item.username }}"
state: present
loop: "{{ users }}"
loop_control:
label: "{{ item.username }}"
become: true
- name: Create user accounts
ansible.builtin.user:
name: "{{ item.username }}"
group: "{{ item.username }}"
groups: "{{ item.groups | default([]) }}"
append: true
shell: "{{ item.shell | default('/bin/bash') }}"
create_home: true
password: "{{ item.password | default(omit) }}"
update_password: on_create
state: present
loop: "{{ users }}"
loop_control:
label: "{{ item.username }}"
become: true
- name: Ensure .ssh directory exists for each user
ansible.builtin.file:
path: "/home/{{ item.username }}/.ssh"
state: directory
owner: "{{ item.username }}"
group: "{{ item.username }}"
mode: "0700"
loop: "{{ users }}"
loop_control:
label: "{{ item.username }}"
become: true
when: item.ssh_key is defined