11 Commits

35 changed files with 816 additions and 139 deletions

View File

@ -5,6 +5,25 @@
This ansible collection provides various roles for installing This ansible collection provides various roles for installing
and configuring basic system utilities like gnupg, ssh etc and configuring basic system utilities like gnupg, ssh etc
- [`elasticsearch`](roles/elasticsearch/README.md): Deploy [elasticsearch](https://www.docker.elastic.co/r/elasticsearch/elasticsearch-oss),
a popular (distributed) search and analytics engine, mostly known by it's
letter "E" in the ELK-stack.
- [`git`](roles/git/README.md): configures git on the target system
- [`gnupg`](roles/gnupg/README.md): configures gnupg on the target system
- [`mariadb`](roles/mariadb/README.md): runs [MariaDB Server](https://mariadb.org/), one of the world's most popular open source relational database
- [`minio`](roles/minio/README.md): Deploy [min.io](https://min.io), an
s3-compatible object storage server, using docker containers.
- [`nginx`](roles/nginx/README.md): [nginx](https://www.nginx.com/),
an advanced load balancer, webserver and reverse proxy.
- [`restic`](roles/restic/README.md): Manage backups using restic
and persist them to a configurable backend.
## License ## License
[CNPLv7+](LICENSE.md): Cooperative Nonviolent Public License [CNPLv7+](LICENSE.md): Cooperative Nonviolent Public License

View File

@ -1,12 +1,11 @@
namespace: finallycoffee namespace: finallycoffee
name: base name: base
version: 0.0.1 version: 0.0.2
readme: README.md readme: README.md
authors: authors:
- Johanna Dorothea Reichmann <transcaffeine@finally.coffee> - transcaffeine <transcaffeine@finally.coffee>
description: Roles for bootstrapping tools like gpg, ssh and git description: Roles for base services which are common dependencies other services like databases
license: license_file: LICENSE.md
- CNPLv7+
build_ignore: build_ignore:
- '*.tar.gz' - '*.tar.gz'
repository: https://git.finally.coffee/finallycoffee/base repository: https://git.finally.coffee/finallycoffee/base

3
meta/runtime.yml Normal file
View File

@ -0,0 +1,3 @@
---
requires_ansible: ">=2.12"

View File

@ -1,20 +0,0 @@
# `debian-proxmox` ansible role
This ansible role can be used to convert a (running and reachable) debian to a proxmox instance.
It automates the instructions from https://pve.proxmox.com/wiki/Install_Proxmox_VE_on_Debian_11_Bullseye.
## Usage
This role will attempt to ensure that the `/etc/hosts` are configured correctly for PVE.
The public IP of the server should be given in `debian_proxmox_public_ip` and defaults to
`ansible_facts['eno1'].ipv4.address`.
The hostname and fqdn should be correctly set in `debian_proxmox_hostname` and `debian_proxmox_fqdn`,
and default to `ansible_hostname` and `ansible_fqdn` respectively.
## Packages
It is recommended to remove the packages for the `os-prober` (which would attempt to add VMs as bootable entries
in the bootloader) and the default linux kernel `linux-image-amd64` / `linux-image-5.10*` (and use the proxmox-provided
kernel instead). This role will remove those packages without asking, so be aware.

View File

@ -1,43 +0,0 @@
---
debian_proxmox_role_required_packages:
- python3-apt
- gpg
debian_proxmox_apt_repo_fingerprint: "28139A2F830BD68478A1A01FDD4BA3917E23BF59"
debian_proxmox_apt_repo_key_url: "https://enterprise.proxmox.com/debian/proxmox-release-{{ ansible_distribution_release }}.gpg"
debian_proxmox_apt_repo_url: "http://download.proxmox.com/debian/pve"
debian_proxmox_apt_repo: "deb [arch=amd64] {{ debian_proxmox_apt_repo_url }} {{ ansible_distribution_release }} pve-no-subscription"
debian_proxmox_apt_packages:
- "proxmox-ve"
- "postfix"
- "open-iscsi"
debian_proxmox_apt_packages_to_remove:
- "linux-image-amd64"
- "linux-image-{{ (ansible_facts['kernel'] | split('-') | first | split('.'))[:2] | join('.') }}*"
- "os-prober"
debian_proxmox_max_reboot_timeout_seconds: 900
debian_proxmox_public_ip: "{{ ansible_facts['eno1'].ipv4.address }}"
debian_proxmox_loopback_ipv4: "{{ ansible_facts['lo'].ipv4.address }}"
debian_proxmox_hostname: "{{ ansible_hostname }}"
debian_proxmox_fqdn: "{{ ansible_fqdn }}"
debian_proxmox_hosts_file:
- ip: "{{ debian_proxmox_loopback_ipv4 }}"
fqdn: localhost.localdomain
aliases:
- localhost
- ip: "{[ debian_proxmox_public_ip }}"
fqdn: "{{ debian_proxmox_fqdn }}"
aliases:
- "{{ debian_proxmox_hostname }}"
- pvelocalhost
- ip: 127.0.1.1
fqdn: "{{ debian_proxmox_hostname }}"
state: absent

View File

@ -1,67 +0,0 @@
---
- name: Ensure python3-apt and gpg is available
apt:
package: "{{ debian_proxmox_role_required_packages }}"
state: present
- name: Check if target is debian before attempting to convert to proxmox
fail:
msg: "Target is {{ ansible_distribution }} which is not Debian"
when: ansible_distribution != 'Debian'
- name: Check if debian version is supported by role
fail:
msg: "{{ ansible_distribution }} {{ ansible_distribution_version }} is not supported by the role"
when: ansible_distribution_version not in debian_proxmox_supported_debian_versions
- name: Ensure /etc/hosts entries are safe for use with proxmox
lineinfile:
dest: /etc/hosts
line: "{{ item.ip }}\t{{ item.fqdn | default('') }}\t{{ item.aliases | default([]) | join('\t') }}"
regex: "{{ item.ip }}.+"
state: "{{ item.state | default('present') }}"
loop: "{{ debian_proxmox_hosts_file }}"
- name: Ensure Proxmox VE apt repository keys are added
apt_key:
id: "{{ debian_proxmox_apt_repo_fingerprint }}"
url: "{{ debian_proxmox_apt_repo_key_url }}"
state: present
- name: Ensure Proxmox VE apt repository is added
apt_repository:
filename: pve-install-repo
repo: "{{ debian_proxmox_apt_repo }}"
state: present
register: proxmox_ve_apt_repo
- name: Ensure APT cache is up to date
apt:
update_cache: yes
when: proxmox_ve_apt_repo.changed
- name: Ensure system is upgraded
apt:
upgrade: full
- name: Ensure Proxmox VE packages are installed
apt:
package: "{{ debian_proxmox_apt_packages }}"
state: present
register: proxmox_ve_installed
- name: Ensure system is rebooted after install of PVE packages
reboot:
reboot_timeout: "{{ debian_proxmox_max_reboot_timeout_seconds | int }}"
when: proxmox_ve_installed.changed
- name: Ensure packages are removed that will conflict with proxmox operation
apt:
package: "{{ debian_proxmox_apt_packages_to_remove }}"
state: absent
register: proxmox_apt_packages_removed
- name: Ensure grub was updated after the kernel was removed
command: update-grub
when: proxmox_apt_packages_removed.changed

View File

@ -1,4 +0,0 @@
---
debian_proxmox_supported_debian_versions:
- 11

View File

@ -0,0 +1,22 @@
# `finallycoffee.base.elastiscsearch`
A simple ansible role which deploys a single-node elastic container to provide
an easy way to do some indexing.
## Usage
Per default, `/opt/elasticsearch/data` is used to persist data, it is
customizable by using either `elasticsearch_base_path` or `elasticsearch_data_path`.
As elasticsearch be can be quite memory heavy, the maximum amount of allowed RAM
can be configured using `elasticsearch_allocated_ram_mb`, defaulting to 512 (mb).
The cluster name and discovery type can be overridden using
`elasticsearch_config_cluster_name` (default: elastic) and
`elasticsearch_config_discovery_type` (default: single-node), should one
need a multi-node elasticsearch deployment.
Per default, no ports or networks are mapped, and explizit mapping using
either ports (`elasticsearch_container_ports`) or networks
(`elasticsearch_container_networks`) is required in order for other services
to use elastic.

View File

@ -0,0 +1,35 @@
---
elasticsearch_version: 7.17.7
elasticsearch_base_path: /opt/elasticsearch
elasticsearch_data_path: "{{ elasticsearch_base_path }}/data"
elasticsearch_config_cluster_name: elastic
elasticsearch_config_discovery_type: single-node
elasticsearch_config_boostrap_memory_lock: true
elasticsearch_allocated_ram_mb: 512
elasticsearch_container_image_name: docker.elastic.co/elasticsearch/elasticsearch-oss
elasticsearch_container_image_tag: ~
elasticsearch_container_image: >-
{{ elasticsearch_container_image_name }}:{{ elasticsearch_container_image_tag | default(elasticsearch_version, true) }}
elasticsearch_container_name: elasticsearch
elasticsearch_container_env:
"ES_JAVA_OPTS": "-Xms{{ elasticsearch_allocated_ram_mb }}m -Xmx{{ elasticsearch_allocated_ram_mb }}m"
"cluster.name": "{{ elasticsearch_config_cluster_name }}"
"discovery.type": "{{ elasticsearch_config_discovery_type }}"
"bootstrap.memory_lock": "{{ 'true' if elasticsearch_config_boostrap_memory_lock else 'false' }}"
elasticsearch_container_user: ~
elasticsearch_container_ports: ~
elasticsearch_container_labels:
version: "{{ elasticsearch_version }}"
elasticsearch_container_ulimits:
# - "memlock:{{ (1.5 * 1024 * elasticsearch_allocated_ram_mb) | int }}:{{ (1.5 * 1024 * elasticsearch_allocated_ram_mb) | int }}"
- "memlock:-1:-1"
elasticsearch_container_volumes:
- "{{ elasticsearch_data_path }}:/usr/share/elasticsearch/data:z"
elasticsearch_container_networks: ~
elasticsearch_container_purge_networks: ~
elasticsearch_container_restart_policy: unless-stopped

View File

@ -0,0 +1,32 @@
---
- name: Ensure host directories are present
file:
path: "{{ item }}"
state: directory
mode: "0777"
loop:
- "{{ elasticsearch_base_path }}"
- "{{ elasticsearch_data_path }}"
- name: Ensure elastic container image is present
docker_image:
name: "{{ elasticsearch_container_image }}"
state: present
source: pull
force_source: "{{ elasticsearch_container_image_tag|default(false, true)|bool }}"
- name: Ensure elastic container is running
docker_container:
name: "{{ elasticsearch_container_name }}"
image: "{{ elasticsearch_container_image }}"
env: "{{ elasticsearch_container_env | default(omit, True) }}"
user: "{{ elasticsearch_container_user | default(omit, True) }}"
ports: "{{ elasticsearch_container_ports | default(omit, True) }}"
labels: "{{ elasticsearch_container_labels | default(omit, True) }}"
volumes: "{{ elasticsearch_container_volumes }}"
ulimits: "{{ elasticsearch_container_ulimits }}"
networks: "{{ elasticsearch_container_networks | default(omit, True) }}"
purge_networks: "{{ elasticsearch_container_purge_networks | default(omit, True) }}"
restart_policy: "{{ elasticsearch_container_restart_policy }}"
state: started

19
roles/git/README.md Normal file
View File

@ -0,0 +1,19 @@
# `finallycoffee.base.git` ansible role
This role configures git for the `ansible_user` and can be used
to (pre)-configure git.
## Examples
```yaml
git_config_user_name: # user name to use for git
git_config_user_email: # email to use for git
git_config_core_editor: vim # editor to use
git_config_credentials:
- remote_url: https://github.com
config:
username: my_github_username
```

4
roles/gnupg/README.md Normal file
View File

@ -0,0 +1,4 @@
# `finallycoffee.base.gnupg` ansible role
Configures GnuPG on the target system, including a Smart-Card (SC) daemon
and can set up gpg-agent as an SSH-agent.

19
roles/mariadb/README.md Normal file
View File

@ -0,0 +1,19 @@
# `finallycoffee.base.mariadb` ansible role
This role deploys a MariaDB instance in a docker container.
## Usage
The role expects the following variables to be populated with values and/or secrets:
```yaml
mariadb_root_password: #mariadb root password
mariadb_database: # name of the database to create
mariadb_username: # name of a user to auto-create and assign permission on the mariadb_database
mariadb_password: # password of the user in mariadb_username
```
## Requirements
- Docker installed
- python-docker present on target system for ansible to be able to talk with the docker API.

View File

@ -0,0 +1,32 @@
---
mariadb_version: "10.6.11"
mariadb_base_path: /var/lib/mariadb
mariadb_data_path: "{{ mariadb_base_path }}/{{ mariadb_version }}"
mariadb_root_password: ~
mariadb_database: ~
mariadb_username: ~
mariadb_password: ~
mariadb_container_base_environment:
MARIADB_ROOT_PASSWORD: "{{ mariadb_root_password }}"
mariadb_container_extra_environment: {}
mariadb_container_name: mariadb
mariadb_container_image_name: docker.io/mariadb
mariadb_container_image_tag: ~
mariadb_container_image: "{{ mariadb_container_image_name }}:{{ mariadb_container_image_tag | default(mariadb_version, true) }}"
mariadb_container_base_volumes:
- "{{ mariadb_data_path }}:{{ mariadb_container_data_path }}:z"
mariadb_container_extra_volumes: []
mariadb_container_base_labels:
version: "{{ mariadb_version }}"
mariadb_container_extra_labels: {}
mariadb_container_restart_policy: "unless-stopped"
mariadb_container_environment: >-2
{{ mariadb_container_base_environment
| combine(mariadb_container_database_environment
if (mariadb_database and mariadb_username and mariadb_password)
else {}, recursive=True)
| combine(mariadb_container_extra_environment) }}

View File

@ -0,0 +1,20 @@
---
- name: Ensure mariaDB container image is present on host
community.docker.docker_image:
name: "{{ mariadb_container_image }}"
state: present
source: pull
- name: Ensure mariaDB {{ mariadb_version }} is running as '{{ mariadb_container_name }}'
community.docker.docker_container:
name: "{{ mariadb_container_name }}"
image: "{{ mariadb_container_image }}"
env: "{{ mariadb_container_environment }}"
ports: "{{ mariadb_container_ports }}"
labels: "{{ mariadb_container_labels }}"
volumes: "{{ mariadb_container_volumes }}"
networks: "{{ mariadb_container_networks | default(omit, true) }}"
etc_hosts: "{{ mariadb_container_etc_hosts | default(omit, true) }}"
purge_networks: "{{ mariadb_container_purge_networks | default(omit, true) }}"
restart_policy: "{{ mariadb_container_restart_policy }}"
state: started

View File

@ -0,0 +1,10 @@
---
mariadb_container_database_environment:
MARIADB_DATABASE: "{{ mariadb_database }}"
MARIADB_USER: "{{ mariadb_username }}"
MARIADB_PASSWORD: "{{ mariadb_password }}"
mariadb_container_data_path: /var/lib/mysql
mariadb_container_volumes: "{{ mariadb_container_base_volumes + mariadb_container_extra_volumes }}"
mariadb_container_labels: "{{ mariadb_container_base_labels | combine(mariadb_container_extra_labels, recursive=True) }}"

29
roles/minio/README.md Normal file
View File

@ -0,0 +1,29 @@
# `finallycoffee.base.minio` ansible role
## Overview
This role deploys a [min.io](https://min.io) server (s3-compatible object storage server)
using the official docker container image.
## Configuration
The role requires setting the password for the `root` user (name can be changed by
setting `minio_root_username`) in `minio_root_password`. That user has full control
over the minio-server instance.
### Useful config hints
Most configuration is done by setting environment variables in
`minio_container_extra_env`, for example:
```yaml
minio_container_extra_env:
# disable the "console" web browser UI
MINIO_BROWSER: off
# enable public prometheus metrics on `/minio/v2/metrics/cluster`
MINIO_PROMETHEUS_AUTH_TYPE: public
```
When serving minio (or any s3-compatible server) on a "subfolder",
see https://docs.aws.amazon.com/AmazonS3/latest/userguide/RESTRedirect.html
and https://docs.aws.amazon.com/AmazonS3/latest/userguide/VirtualHosting.html

View File

@ -0,0 +1,40 @@
---
minio_user: ~
minio_data_path: /opt/minio
minio_create_user: false
minio_manage_host_filesystem: false
minio_root_username: root
minio_root_password: ~
minio_container_name: minio
minio_container_image_name: docker.io/minio/minio
minio_container_image_tag: latest
minio_container_image: "{{ minio_container_image_name }}:{{ minio_container_image_tag }}"
minio_container_networks: []
minio_container_ports: []
minio_container_base_volumes:
- "{{ minio_data_path }}:{{ minio_container_data_path }}:z"
minio_container_extra_volumes: []
minio_container_base_env:
MINIO_ROOT_USER: "{{ minio_root_username }}"
MINIO_ROOT_PASSWORD: "{{ minio_root_password }}"
minio_container_extra_env: {}
minio_container_labels: {}
minio_container_command:
- "server"
- "{{ minio_container_data_path }}"
- "--console-address \":{{ minio_container_listen_port_console }}\""
minio_container_restart_policy: "unless-stopped"
minio_container_image_force_source: "{{ (minio_container_image_tag == 'latest')|bool }}"
minio_container_listen_port_api: 9000
minio_container_listen_port_console: 8900
minio_container_data_path: /storage

View File

@ -0,0 +1,37 @@
---
- name: Ensure minio run user is present
user:
name: "{{ minio_user }}"
state: present
system: yes
when: minio_create_user
- name: Ensure filesystem mounts ({{ minio_data_path }}) for container volumes are present
file:
path: "{{ minio_data_path }}"
state: directory
user: "{{ minio_user|default(omit, True) }}"
group: "{{ minio_user|default(omit, True) }}"
when: minio_manage_host_filesystem
- name: Ensure container image for minio is present
community.docker.docker_image:
name: "{{ minio_container_image }}"
state: present
source: pull
force_source: "{{ minio_container_image_force_source }}"
- name: Ensure container {{ minio_container_name }} is running
docker_container:
name: "{{ minio_container_name }}"
image: "{{ minio_container_image }}"
volumes: "{{ minio_container_volumes }}"
env: "{{ minio_container_env }}"
labels: "{{ minio_container_labels }}"
networks: "{{ minio_container_networks }}"
ports: "{{ minio_container_ports }}"
user: "{{ minio_user|default(omit, True) }}"
command: "{{ minio_container_command }}"
restart_policy: "{{ minio_container_restart_policy }}"
state: started

View File

@ -0,0 +1,5 @@
---
minio_container_volumes: "{{ minio_container_base_volumes + minio_container_extra_volumes }}"
minio_container_env: "{{ minio_container_base_env | combine(minio_container_extra_env) }}"

28
roles/nginx/README.md Normal file
View File

@ -0,0 +1,28 @@
# `finallycoffee.services.nginx` ansible role
## Description
Runs `nginx`, a HTTP reverse proxy, in a docker container.
## Usage
For the role to do anything, `nginx_config` needs to be populated with the configuration for nginx.
An example would be:
```yaml
nginx_config: |+
server {
listen 80 default_server;
server_name my.server.fqdn;
location / { return 200; }
}
```
The container is named `nginx` by default, this can be overridden in `nginx_container_name`.
When running this role multiple times, `nginx_base_path` should also be changed for each run,
otherwise the configuration files collide in the filesystem.
For exposing this server to the host and/or internet, the `nginx_container_ports` (port forwarding host
from host to container), `nginx_container_networks` (docker networking) or `nginx_container_labels`
(for label-based routing discovery like traefik) can be used. The options correspond to the arguments
of the `community.docker.docker_container` module.

View File

@ -0,0 +1,33 @@
---
nginx_version: "1.25.1"
nginx_flavour: alpine
nginx_base_path: /opt/nginx
nginx_config_file: "{{ nginx_base_path }}/nginx.conf"
nginx_container_name: nginx
nginx_container_image_reference: >-
{{
nginx_container_image_repository
+ ':' + (nginx_container_image_tag
| default(nginx_version
+ (('-' + nginx_flavour) if nginx_flavour is defined else ''), true))
}}
nginx_container_image_repository: >-
{{
(
container_registries[nginx_container_image_registry]
| default(nginx_container_image_registry)
)
+ '/'
+ nginx_container_image_namespace | default('')
+ nginx_container_image_name
}}
nginx_container_image_registry: "docker.io"
nginx_container_image_name: "nginx"
nginx_container_image_tag: ~
nginx_container_restart_policy: "unless-stopped"
nginx_container_volumes:
- "{{ nginx_config_file }}:/etc/nginx/conf.d/nginx.conf:ro"

View File

@ -0,0 +1,8 @@
---
- name: Ensure nginx container '{{ nginx_container_name }}' is restarted
community.docker.docker_container:
name: "{{ nginx_container_name }}"
state: started
restart: true
listen: restart-nginx

View File

@ -0,0 +1,37 @@
---
- name: Ensure base path '{{ nginx_base_path }}' exists
ansible.builtin.file:
path: "{{ nginx_base_path }}"
state: directory
mode: 0755
- name: Ensure nginx config file is templated
ansible.builtin.copy:
dest: "{{ nginx_config_file }}"
content: "{{ nginx_config }}"
mode: 0640
notify:
- restart-nginx
- name: Ensure docker container image is present
community.docker.docker_image:
name: "{{ nginx_container_image_reference }}"
state: present
source: pull
force_source: "{{ nginx_container_image_tag is defined and nginx_container_image_tag | string != '' }}"
- name: Ensure docker container '{{ nginx_container_name }}' is running
community.docker.docker_container:
name: "{{ nginx_container_name }}"
image: "{{ nginx_container_image_reference }}"
env: "{{ nginx_container_env | default(omit, true) }}"
user: "{{ nginx_container_user | default(omit, true) }}"
ports: "{{ nginx_container_ports | default(omit, true) }}"
labels: "{{ nginx_container_labels | default(omit, true) }}"
volumes: "{{ nginx_container_volumes | default(omit, true) }}"
etc_hosts: "{{ nginx_container_etc_hosts | default(omit, true) }}"
networks: "{{ nginx_container_networks | default(omit, true) }}"
purge_networks: "{{ nginx_container_purge_networks | default(omit, true) }}"
restart_policy: "{{ nginx_container_restart_policy }}"
state: started

13
roles/redis/README.md Normal file
View File

@ -0,0 +1,13 @@
---
# `finallycoffee.base.redis` ansible role
Ansible role to deploy redis. Can use systemd or docker, depending on the
value of `redis_deployment_method`. Supports running the role multiple times
by setting `redis_instance` to a unique string to avoid namespace-collisions.
## Configuration
Extra configurations keys for redis can be provided as key-value pairs
in `redis_config`. For all configuration keys, consult the upstream example
redis.conf.

View File

@ -0,0 +1,51 @@
---
redis_instance: ''
redis_version: "7.2"
redis_user: "redis{{ '-' ~ redis_instance }}"
redis_deployment_method: docker
redis_config_file: "/etc/redis/redis{{ '-' ~ redis_instance }}.conf"
redis_data_directory: "/var/lib/redis/"
redis_config_dbfilename: "redis{{ '-' ~ redis_instance }}.rdb"
redis_config_dir: "{{ redis_data_directory }}"
redis_config_bind:
- -::1
- "{{ (redis_deployment_method == 'docker') | ternary('0.0.0.0', '127.0.0.1') }}"
- "{{ (redis_deployment_method == 'docker') | ternary('-::*', '::1') }}"
redis_config_port: "6379"
redis_config_procted_mode: true
#redis_config_maxmemory_bytes: 100mb
#redis_config_maxmemory_policy: noeviction
redis_config_unix_socket: "/run/redis.sock"
redis_config_unix_socket_perm: "700"
redis_container_name: "redis{{ '_' ~ redis_instance }}"
redis_container_image_flavour: alpine
redis_container_image_registry: "docker.io"
redis_container_image_namespace: ~
redis_container_image_name: "redis"
redis_container_image_reference: >-
{{ redis_container_image_repository ~ ':'
~ redis_container_image_tage | default(
redis_version ~ (redis_container_image_flavour | ternary(
'-' ~ redis_container_image_flavour, '')), true) }}
redis_container_image_repository: >-
{{ redis_container_image_registry ~ '/'
~ (redis_container_image_namespace | ternary(redis_container_image_namespace ~ '/'))
~ redis_container_image_name }}
redis_container_ports:
- "127.0.0.1:{{ redis_config_port }}:{{ redis_config_port }}"
- "[i::1]:{{ redis_config_port }}:{{ redis_config_port }}"
redis_container_restart_policy: "unless-stopped"
redis_container_state: "started"
redis_container_base_labels:
version: "{{ redis_version }}"
redis_container_all_labels: >-
{{ redis_container_base_labels | combine(redis_container_labels | default({})) }}
redis_container_base_volumes:
- "{{ redis_config_file }}:/usr/local/etc/redis/redis.conf:ro"
- "{{ redis_data_directory }}:{{ redis_data_directory }}:rw"
redis_container_all_volumes: >-
{{ redis_container_base_volumes + redis_container_volumes | default([]) }}

View File

@ -0,0 +1,11 @@
---
- name: Ensure redis container '{{ redis_container_name }}' is restarted
listen: restart-redis
community.docker.docker_container:
name: "{{ redis_container_image }}"
state: "started"
restart: true
when:
- redis_deployment_method == "docker"
- not redis_container_info.changed

View File

@ -0,0 +1,50 @@
---
- name: Ensure redis user '{{ redis_user }}' is present
ansible.builtin.user:
name: "{{ redis_user }}"
state: "present"
system: true
create_home: false
groups: "{{ redis_user_groups | default(omit) }}"
append: "{{ redis_user_groups is defined | ternary('true', omit) }}"
register: redis_user_info
- name: Ensure redis configuration is written out
ansible.builtin.copy:
content: |+
{% for key, value in redis_config_to_write %}
{{ key }} {{ value }}
{% endfor %}
dest: "{{ redis_config_file }}"
owner: "{{ redis_user_info.uid | default(redis_user) }}"
group: "{{ redis_user_info.group | default(redis_user) }}"
mode: "0640"
notify:
- restart-redis
- name: Ensure container image is present on host
community.docker.docker_image:
name: "{{ redis_container_image_reference }}"
state: "present"
source: "pull"
force_source: "{{ redis_container_image_tag | bool }}"
when: "redis_deployment_method == 'docker'"
- name: Ensure redis container '{{ redis_container_name }}' is '{{ redis_container_state }}'
community.docker.docker_container:
name: "{{ redis_container_name }}"
image: "{{ redis_container_image_reference }}"
env: "{{ redis_container_env | default(omit) }}"
ports: "{{ redis_container_ports | default(omit) }}"
labels: "{{ redis_container_all_labels }}"
volumes: "{{ redis_container_all_volumes }}"
networks: "{{ redis_container_networks | default(omit) }}"
purge_networks: "{{ redis_container_purge_networks | default(omit) }}"
etc_hosts: "{{ redis_container_etc_hosts | default(omit) }}"
memory: "{{ redis_container_memory | default(omit) }}"
memory_swap: "{{ redis_container_memory_swap | default(omit) }}"
restart_policy: "{{ redis_container_restart_policy }}"
state: "{{ redis_container_state }}"
register: redis_container_info
when: "redis_deployment_method == 'docker'"

13
roles/redis/vars/main.yml Normal file
View File

@ -0,0 +1,13 @@
---
redis_base_config:
dbfilename: "{{ redis_config_dbfilename }}"
dir: "{{ redis_data_directory }}"
bind: "{{ redis_config_bind | join(' ') }}"
port: "{{ redis_config_port }}"
"protected-mode": "{{ redis_config_protected_mode | ternary('yes', 'no') }}"
unixsocket: "{{ redis_config_unix_socket }}"
unixsocketperm: "{{ redis_config_unix_socket_perm }}"
redis_config_to_write: >-
{{ redis_base_config | combine(redis_config | default({}), recursive=True) }}

77
roles/restic/README.md Normal file
View File

@ -0,0 +1,77 @@
# `finallycoffee.base.restic`
Ansible role for backup up data using `restic`, utilizing `systemd` timers for scheduling.
## Overview
As restic encrypts the data before storing it, the `restic_repo_password` needs
to be populated with a strong key, and saved accordingly as only this key can
be used to decrypt the data for a restore!
### Backends
#### S3 Backend
To use a `s3`-compatible backend like AWS buckets or minio, both `restic_s3_key_id`
and `restic_s3_access_key` need to be populated, and the `restic_repo_url` has the
format `s3:https://my.s3.endpoint:port/bucket-name`.
#### SFTP Backend
Using the `sftp` backend requires the configured `restic_user` to be able to
authenticate to the configured SFTP-Server using password-less methods like
publickey-authentication. The `restic_repo_url` then follows the format
`sftp:{user}@{server}:/my-restic-repository` (or without leading `/` for relative
paths to the `{user}`s home directory.
### Backing up data
A job name like `$service-postgres` or similar needs to be set in `restic_job_name`,
which is used for naming the `systemd` units, their syslog identifiers etc.
If backing up filesystem locations, the paths need to be specified in
`restic_backup_paths` as lists of strings representing absolute filesystem
locations.
If backing up f.ex. database or other data which is generating backups using
a command like `pg_dump`, use `restic_backup_stdin_command` (which needs to output
to `stdout`) in conjunction with `restic_backup_stdin_command_filename` to name
the resulting output (required).
### Policy
The backup policy can be adjusted by overriding the `restic_policy_keep_*`
variables, with the defaults being:
```yaml
restic_policy_keep_all_within: 1d
restic_policy_keep_hourly: 6
restic_policy_keep_daily: 2
restic_policy_keep_weekly: 7
restic_policy_keep_monthly: 4
restic_policy_backup_frequency: hourly
```
**Note:** `restic_policy_backup_frequency` must conform to `systemd`s
`OnCalendar` syntax, which can be checked using `systemd-analyze calender $x`.
## Role behaviour
Per default, when the systemd unit for a job changes, the job is not immediately
started. This can be overridden using `restic_start_job_on_unit_change: true`,
which will immediately start the backup job if it's configuration changed.
The systemd unit runs with `restic_user`, which is root by default, guaranteeing
that filesystem paths are always readable. The `restic_user` can be overridden,
but care needs to be taken to ensure the user has permission to read all the
provided filesystem paths / the backup command may be executed by the user.
If ansible should create the user, set `restic_create_user` to `true`, which
will attempt to create the `restic_user` as a system user.
### Installing
For Debian and RedHat, the role attempts to install restic using the default
package manager's ansible module (apt/dnf). For other distributions, the generic
`package` module tries to install `restic_package_name` (default: `restic`),
which can be overridden if needed.

View File

@ -0,0 +1,37 @@
---
restic_repo_url: ~
restic_repo_password: ~
restic_s3_key_id: ~
restic_s3_access_key: ~
restic_backup_paths: []
restic_backup_stdin_command: ~
restic_backup_stdin_command_filename: ~
restic_policy_keep_all_within: 1d
restic_policy_keep_hourly: 6
restic_policy_keep_daily: 2
restic_policy_keep_weekly: 7
restic_policy_keep_monthly: 4
restic_policy_backup_frequency: hourly
restic_policy:
keep_within: "{{ restic_policy_keep_all_within }}"
hourly: "{{ restic_policy_keep_hourly }}"
daily: "{{ restic_policy_keep_daily }}"
weekly: "{{ restic_policy_keep_weekly }}"
monthly: "{{ restic_policy_keep_monthly }}"
frequency: "{{ restic_policy_backup_frequency }}"
restic_user: root
restic_create_user: false
restic_start_job_on_unit_change: false
restic_job_name: ~
restic_job_description: "Restic backup job for {{ restic_job_name }}"
restic_systemd_unit_naming_scheme: "restic.{{ restic_job_name }}"
restic_systemd_working_directory: /tmp
restic_systemd_syslog_identifier: "restic-{{ restic_job_name }}"
restic_package_name: restic

View File

@ -0,0 +1,13 @@
---
- name: Ensure system daemon is reloaded
listen: reload-systemd
systemd:
daemon_reload: true
- name: Ensure systemd service for '{{ restic_job_name }}' is started immediately
listen: trigger-restic
systemd:
name: "{{ restic_systemd_unit_naming_scheme }}.service"
state: started
when: restic_start_job_on_unit_change

View File

@ -0,0 +1,77 @@
---
- name: Ensure {{ restic_user }} system user exists
user:
name: "{{ restic_user }}"
state: present
system: true
when: restic_create_user
- name: Ensure either backup_paths or backup_stdin_command is populated
when: restic_backup_paths|length > 0 and restic_backup_stdin_command
fail:
msg: "Setting both `restic_backup_paths` and `restic_backup_stdin_command` is not supported"
- name: Ensure a filename for stdin_command backup is given
when: restic_backup_stdin_command and not restic_backup_stdin_command_filename
fail:
msg: "`restic_backup_stdin_command` was set but no filename for the resulting output was supplied in `restic_backup_stdin_command_filename`"
- name: Ensure backup frequency adheres to systemd's OnCalender syntax
command:
cmd: "systemd-analyze calendar {{ restic_policy.frequency }}"
register: systemd_calender_parse_res
failed_when: systemd_calender_parse_res.rc != 0
changed_when: false
- name: Ensure restic is installed
block:
- name: Ensure restic is installed via apt
apt:
package: restic
state: latest
when: ansible_os_family == 'Debian'
- name: Ensure restic is installed via dnf
dnf:
name: restic
state: latest
when: ansible_os_family == 'RedHat'
- name: Ensure restic is installed using the auto-detected package-manager
package:
name: "{{ restic_package_name }}"
state: present
when: ansible_os_family not in ['RedHat', 'Debian']
- name: Ensure systemd service file for '{{ restic_job_name }}' is templated
template:
dest: "/etc/systemd/system/{{ restic_systemd_unit_naming_scheme }}.service"
src: restic.service.j2
owner: root
group: root
mode: 0640
notify:
- reload-systemd
- trigger-restic
- name: Ensure systemd service file for '{{ restic_job_name }}' is templated
template:
dest: "/etc/systemd/system/{{ restic_systemd_unit_naming_scheme }}.timer"
src: restic.timer.j2
owner: root
group: root
mode: 0640
notify:
- reload-systemd
- name: Flush handlers to ensure systemd knows about '{{ restic_job_name }}'
meta: flush_handlers
- name: Ensure systemd timer for '{{ restic_job_name }}' is activated
systemd:
name: "{{ restic_systemd_unit_naming_scheme }}.timer"
enabled: true
- name: Ensure systemd timer for '{{ restic_job_name }}' is started
systemd:
name: "{{ restic_systemd_unit_naming_scheme }}.timer"
state: started

View File

@ -0,0 +1,28 @@
[Unit]
Description={{ restic_job_description }}
[Service]
Type=oneshot
User={{ restic_user }}
WorkingDirectory={{ restic_systemd_working_directory }}
SyslogIdentifier={{ restic_systemd_syslog_identifier }}
Environment=RESTIC_REPOSITORY={{ restic_repo_url }}
Environment=RESTIC_PASSWORD={{ restic_repo_password }}
{% if restic_s3_key_id and restic_s3_access_key %}
Environment=AWS_ACCESS_KEY_ID={{ restic_s3_key_id }}
Environment=AWS_SECRET_ACCESS_KEY={{ restic_s3_access_key }}
{% endif %}
ExecStartPre=-/bin/sh -c '/usr/bin/restic snapshots || /usr/bin/restic init'
{% if restic_backup_stdin_command %}
ExecStart=/bin/sh -c '{{ restic_backup_stdin_command }} | /usr/bin/restic backup --verbose --stdin --stdin-filename {{ restic_backup_stdin_command_filename }}'
{% else %}
ExecStart=/usr/bin/restic --verbose backup {{ restic_backup_paths | join(' ') }}
{% endif %}
ExecStartPost=/usr/bin/restic forget --prune --keep-within={{ restic_policy.keep_within }} --keep-hourly={{ restic_policy.hourly }} --keep-daily={{ restic_policy.daily }} --keep-weekly={{ restic_policy.weekly }} --keep-monthly={{ restic_policy.monthly }}
ExecStartPost=-/usr/bin/restic snapshots
ExecStartPost=/usr/bin/restic check
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,10 @@
[Unit]
Description=Run {{ restic_job_name }}
[Timer]
OnCalendar={{ restic_policy.frequency }}
Persistent=True
Unit={{ restic_systemd_unit_naming_scheme }}.service
[Install]
WantedBy=timers.target