feat(infrastructure): introduce ansible deployment for taler services

- add ansible playbooks and roles for deploying taler exchange and merchant
- configure PostgreSQL, GNU Taler repositories, and services

🔧 chore(config): provide example env and gitignore for sensitive files

- create .env-example with placeholders for sensitive info
- add .gitignore entries for .env and common editor files
This commit is contained in:
nocci 2025-11-03 13:15:46 +00:00
parent ca89a06864
commit fdc9234540
18 changed files with 525 additions and 0 deletions

39
.env-example Normal file
View file

@ -0,0 +1,39 @@
# Core domain and contact settings
TALER_DOMAIN=demogeld.example
TALER_ADMIN_EMAIL=ops@demogeld.example
LETSENCRYPT_CONTACT=mailto:ops@demogeld.example
# PostgreSQL access
TALER_DB_HOST=localhost
TALER_DB_PORT=5432
TALER_DB_ADMIN_USER=postgres
TALER_DB_ADMIN_PASSWORD=CHANGE_ME
TALER_EXCHANGE_DB=talerexchange
TALER_EXCHANGE_DB_USER=taler_exchange
TALER_EXCHANGE_DB_PASSWORD=CHANGE_ME
TALER_MERCHANT_DB=talermerchant
TALER_MERCHANT_DB_USER=taler_merchant
TALER_MERCHANT_DB_PASSWORD=CHANGE_ME
# Service hostnames
TALER_EXCHANGE_HOST=exchange.demogeld.example
TALER_MERCHANT_HOST=merchant.demogeld.example
TALER_PAY_HOST=pay.demogeld.example
WORDPRESS_HOST=www.demogeld.example
# Package mirrors / versions
TALER_PACKAGE_REPO=https://deb.taler.net/
TALER_DISTRIBUTION=stable
# TLS
ACME_EMAIL=ops@demogeld.example
# Optional WordPress database details (if managed here)
WORDPRESS_DB_NAME=wp_demogeld
WORDPRESS_DB_USER=wp_demogeld
WORDPRESS_DB_PASSWORD=CHANGE_ME
# Exchange master public key (never commit the real value)
TALER_MASTER_PUBLIC_KEY=REPLACE_WITH_PUBLIC_KEY
# Currency definition
TALER_CURRENCY=DEMOGELD

7
.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
.env
__pycache__/
*.retry
ansible.log
.vscode/
.idea/

12
ansible.cfg Normal file
View file

@ -0,0 +1,12 @@
[defaults]
inventory = inventories/local/hosts.yml
roles_path = roles
collections_paths = ./collections
pipelining = True
retry_files_enabled = False
interpreter_python = auto
[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=60s
control_path = ~/.ansible/cp/%%h-%%p-%%r

View file

@ -0,0 +1,6 @@
---
collections:
- name: community.general
version: ">=9.0.0"
- name: community.postgresql
version: ">=3.0.0"

31
group_vars/all.yml Normal file
View file

@ -0,0 +1,31 @@
---
# Default values that can be overridden via .env
taler_defaults:
TALER_DOMAIN: demogeld.example
TALER_ADMIN_EMAIL: ops@demogeld.example
LETSENCRYPT_CONTACT: mailto:ops@demogeld.example
TALER_PACKAGE_REPO: https://deb.taler.net/
TALER_DISTRIBUTION: stable
TALER_CURRENCY: DEMOGELD
TALER_DB_HOST: localhost
TALER_DB_PORT: "5432"
TALER_DB_ADMIN_USER: postgres
TALER_DB_ADMIN_PASSWORD: ""
TALER_EXCHANGE_DB: talerexchange
TALER_EXCHANGE_DB_USER: taler_exchange
TALER_EXCHANGE_DB_PASSWORD: ""
TALER_MERCHANT_DB: talermerchant
TALER_MERCHANT_DB_USER: taler_merchant
TALER_MERCHANT_DB_PASSWORD: ""
TALER_EXCHANGE_HOST: exchange.demogeld.example
TALER_MERCHANT_HOST: merchant.demogeld.example
TALER_PAY_HOST: pay.demogeld.example
WORDPRESS_HOST: www.demogeld.example
WORDPRESS_DB_NAME: wp_demogeld
WORDPRESS_DB_USER: wp_demogeld
WORDPRESS_DB_PASSWORD: ""
TALER_MASTER_PUBLIC_KEY: ""

View file

@ -0,0 +1,12 @@
---
all:
children:
taler_exchange:
hosts:
localhost:
ansible_connection: local
taler_merchant:
hosts:
localhost:
ansible_connection: local

28
playbooks/site.yml Normal file
View file

@ -0,0 +1,28 @@
---
- name: Bootstrap shared configuration
hosts: all
become: true
gather_facts: true
roles:
- env_config
- common
- taler_repo
- name: Prepare PostgreSQL for Taler services
hosts: taler_exchange
become: true
roles:
- postgres
- name: Deploy Taler exchange
hosts: taler_exchange
become: true
roles:
- taler_exchange
- name: Deploy Taler merchant backend
hosts: taler_merchant
become: true
roles:
- taler_merchant

View file

@ -0,0 +1,17 @@
---
- name: Ensure apt cache is up to date
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
- name: Install base packages
ansible.builtin.apt:
name:
- gnupg2
- apt-transport-https
- ca-certificates
- curl
- logrotate
- python3-apt
state: present

View file

@ -0,0 +1,66 @@
---
- name: Determine project root
set_fact:
project_root: "{{ (playbook_dir | realpath) | dirname }}"
- name: Set default .env path
set_fact:
env_file_resolved: "{{ lookup('ansible.builtin.env', 'GNUTALER_ENV_FILE') }}"
- name: Override .env path with project default when not provided
set_fact:
env_file_resolved: "{{ project_root + '/.env' }}"
when: env_file_resolved | default('') | length == 0
- name: Check if .env file exists
ansible.builtin.stat:
path: "{{ env_file_resolved }}"
register: env_file_stat
- name: Load .env overrides when present
set_fact:
env_overrides: "{{ lookup('community.general.read_env', env_file_resolved) }}"
when: env_file_stat.stat.exists
- name: Warn when .env file is missing
ansible.builtin.debug:
msg: >-
.env file not found at {{ env_file_resolved }}.
Using defaults from group_vars/all.yml. Create one from .env-example.
when: not env_file_stat.stat.exists
- name: Merge defaults with .env overrides
set_fact:
taler_env: "{{ taler_defaults | combine(env_overrides | default({}), recursive=True) }}"
- name: Build structured variables for play consumption
set_fact:
taler_config:
domain: "{{ taler_env.TALER_DOMAIN }}"
admin_email: "{{ taler_env.TALER_ADMIN_EMAIL }}"
acme_contact: "{{ taler_env.LETSENCRYPT_CONTACT }}"
package_repo: "{{ taler_env.TALER_PACKAGE_REPO }}"
distribution: "{{ taler_env.TALER_DISTRIBUTION }}"
currency: "{{ taler_env.TALER_CURRENCY }}"
exchange_host: "{{ taler_env.TALER_EXCHANGE_HOST }}"
merchant_host: "{{ taler_env.TALER_MERCHANT_HOST }}"
pay_host: "{{ taler_env.TALER_PAY_HOST }}"
wordpress_host: "{{ taler_env.WORDPRESS_HOST }}"
exchange_master_key: "{{ taler_env.TALER_MASTER_PUBLIC_KEY }}"
db:
host: "{{ taler_env.TALER_DB_HOST }}"
port: "{{ taler_env.TALER_DB_PORT | int }}"
admin_user: "{{ taler_env.TALER_DB_ADMIN_USER }}"
admin_password: "{{ taler_env.TALER_DB_ADMIN_PASSWORD }}"
exchange:
name: "{{ taler_env.TALER_EXCHANGE_DB }}"
user: "{{ taler_env.TALER_EXCHANGE_DB_USER }}"
password: "{{ taler_env.TALER_EXCHANGE_DB_PASSWORD }}"
merchant:
name: "{{ taler_env.TALER_MERCHANT_DB }}"
user: "{{ taler_env.TALER_MERCHANT_DB_USER }}"
password: "{{ taler_env.TALER_MERCHANT_DB_PASSWORD }}"
wordpress_db:
name: "{{ taler_env.WORDPRESS_DB_NAME }}"
user: "{{ taler_env.WORDPRESS_DB_USER }}"
password: "{{ taler_env.WORDPRESS_DB_PASSWORD }}"

View file

@ -0,0 +1,53 @@
---
- name: Ensure PostgreSQL server packages are present
ansible.builtin.apt:
name:
- postgresql
- python3-psycopg2
state: present
- name: Ensure PostgreSQL service is running
ansible.builtin.service:
name: postgresql
state: started
enabled: true
- name: Create exchange database user
community.postgresql.postgresql_user:
name: "{{ taler_config.db.exchange.user }}"
password: "{{ taler_config.db.exchange.password | default(omit) }}"
login_user: "{{ taler_config.db.admin_user }}"
login_password: "{{ taler_config.db.admin_password | default(omit) }}"
login_host: "{{ taler_config.db.host }}"
port: "{{ taler_config.db.port }}"
no_log: true
- name: Create merchant database user
community.postgresql.postgresql_user:
name: "{{ taler_config.db.merchant.user }}"
password: "{{ taler_config.db.merchant.password | default(omit) }}"
login_user: "{{ taler_config.db.admin_user }}"
login_password: "{{ taler_config.db.admin_password | default(omit) }}"
login_host: "{{ taler_config.db.host }}"
port: "{{ taler_config.db.port }}"
no_log: true
- name: Ensure exchange database exists
community.postgresql.postgresql_db:
name: "{{ taler_config.db.exchange.name }}"
owner: "{{ taler_config.db.exchange.user }}"
encoding: UTF8
login_user: "{{ taler_config.db.admin_user }}"
login_password: "{{ taler_config.db.admin_password | default(omit) }}"
login_host: "{{ taler_config.db.host }}"
port: "{{ taler_config.db.port }}"
- name: Ensure merchant database exists
community.postgresql.postgresql_db:
name: "{{ taler_config.db.merchant.name }}"
owner: "{{ taler_config.db.merchant.user }}"
encoding: UTF8
login_user: "{{ taler_config.db.admin_user }}"
login_password: "{{ taler_config.db.admin_password | default(omit) }}"
login_host: "{{ taler_config.db.host }}"
port: "{{ taler_config.db.port }}"

View file

@ -0,0 +1,26 @@
---
- name: Restart taler-exchange-httpd
ansible.builtin.service:
name: taler-exchange-httpd
state: restarted
- name: Restart taler-exchange-aggregator
ansible.builtin.service:
name: taler-exchange-aggregator
state: restarted
- name: Restart taler-exchange-closer
ansible.builtin.service:
name: taler-exchange-closer
state: restarted
- name: Restart taler-exchange-transfer
ansible.builtin.service:
name: taler-exchange-transfer
state: restarted
- name: Restart taler-exchange-wirewatch
ansible.builtin.service:
name: taler-exchange-wirewatch
state: restarted

View file

@ -0,0 +1,63 @@
- name: Ensure master public key is provided
ansible.builtin.assert:
that:
- taler_config.exchange_master_key is defined
- taler_config.exchange_master_key | length > 0
fail_msg: "Provide TALER_MASTER_PUBLIC_KEY in .env before running exchange deployment."
- name: Install GNU Taler exchange packages
ansible.builtin.apt:
name:
- taler-exchange
- taler-exchange-httpd
- taler-exchange-aggregator
- taler-exchange-closer
- taler-exchange-transfer
- taler-exchange-wirewatch
state: present
- name: Ensure exchange runtime directories exist
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: taler-exchange-httpd
group: taler-exchange-httpd
mode: "0750"
loop:
- /var/lib/taler/exchange
- /etc/taler/conf.d
- name: Deploy exchange configuration
ansible.builtin.template:
src: exchange.conf.j2
dest: /etc/taler/conf.d/exchange.conf
owner: root
group: root
mode: "0640"
notify:
- Restart taler-exchange-httpd
- Restart taler-exchange-aggregator
- Restart taler-exchange-closer
- Restart taler-exchange-transfer
- Restart taler-exchange-wirewatch
- name: Run taler-exchange-dbinit
ansible.builtin.command:
cmd: taler-exchange-dbinit -c /etc/taler/conf.d/exchange.conf
register: dbinit_result
changed_when: "'already initialized' not in dbinit_result.stdout"
failed_when: dbinit_result.rc not in [0]
become: true
become_user: taler-exchange-httpd
- name: Validate exchange configuration
ansible.builtin.command:
cmd: taler-config -c /etc/taler/conf.d/exchange.conf -s exchange -o CURRENCY
register: exchange_config_validation
changed_when: false
- name: Assert exchange currency matches expected
ansible.builtin.assert:
that:
- exchange_config_validation.stdout.strip() == taler_config.currency
fail_msg: "Exchange currency does not match expected value {{ taler_config.currency }}. Check configuration."

View file

@ -0,0 +1,47 @@
# Generated by Ansible - manual edits will be overwritten.
[exchange]
BASE_URL = https://{{ taler_config.exchange_host }}/
CURRENCY = {{ taler_config.currency }}
DB = postgres
MASTER_PUBLIC_KEY = {{ taler_config.exchange_master_key }}
IDLE_RESERVE_EXPIRATION_TIME = 4 weeks
RESHARE_RESERVE_EXPIRATION_TIME = 8 weeks
{% set exchange_user = taler_config.db.exchange.user %}
{% set exchange_pass = taler_config.db.exchange.password %}
{% set exchange_auth = exchange_user if not exchange_pass else exchange_user ~ ':' ~ exchange_pass %}
[exchangedb-postgres]
CONFIG = postgres://{{ exchange_auth }}@{{ taler_config.db.host }}:{{ taler_config.db.port }}/{{ taler_config.db.exchange.name }}
[coin_{{ taler_config.currency | lower }}_1]
VALUE = {{ taler_config.currency }}:0.50
DURATION_WITHDRAW = 12 weeks
DURATION_SPEND = 52 weeks
DURATION_DEPOSIT = 104 weeks
DURATION_LEGAL = 208 weeks
FEE_WITHDRAW = 0.00 {{ taler_config.currency }}
FEE_DEPOSIT = 0.00 {{ taler_config.currency }}
FEE_REFRESH = 0.00 {{ taler_config.currency }}
FEE_REFUND = 0.00 {{ taler_config.currency }}
[coin_{{ taler_config.currency | lower }}_2]
VALUE = {{ taler_config.currency }}:1.00
DURATION_WITHDRAW = 12 weeks
DURATION_SPEND = 52 weeks
DURATION_DEPOSIT = 104 weeks
DURATION_LEGAL = 208 weeks
FEE_WITHDRAW = 0.00 {{ taler_config.currency }}
FEE_DEPOSIT = 0.00 {{ taler_config.currency }}
FEE_REFRESH = 0.00 {{ taler_config.currency }}
FEE_REFUND = 0.00 {{ taler_config.currency }}
[coin_{{ taler_config.currency | lower }}_3]
VALUE = {{ taler_config.currency }}:5.00
DURATION_WITHDRAW = 12 weeks
DURATION_SPEND = 52 weeks
DURATION_DEPOSIT = 104 weeks
DURATION_LEGAL = 208 weeks
FEE_WITHDRAW = 0.00 {{ taler_config.currency }}
FEE_DEPOSIT = 0.00 {{ taler_config.currency }}
FEE_REFRESH = 0.00 {{ taler_config.currency }}
FEE_REFUND = 0.00 {{ taler_config.currency }}

View file

@ -0,0 +1,6 @@
---
- name: Restart taler-merchant-backend
ansible.builtin.service:
name: taler-merchant-backend
state: restarted

View file

@ -0,0 +1,69 @@
---
- name: Install GNU Taler merchant packages
ansible.builtin.apt:
name:
- taler-merchant
- taler-merchant-httpd
- taler-merchant-db
state: present
- name: Ensure merchant runtime paths exist
ansible.builtin.file:
path: "{{ item.path }}"
state: directory
owner: "{{ item.owner }}"
group: "{{ item.group }}"
mode: "{{ item.mode }}"
loop:
- { path: /etc/taler, owner: root, group: root, mode: "0755" }
- { path: /etc/taler/merchant.d, owner: root, group: root, mode: "0750" }
- { path: /run/taler, owner: taler-merchant, group: taler-merchant, mode: "0770" }
- name: Deploy merchant configuration
ansible.builtin.template:
src: merchant.conf.j2
dest: /etc/taler/merchant.conf
owner: root
group: taler-merchant
mode: "0640"
notify:
- Restart taler-merchant-backend
- name: Initialize merchant database
ansible.builtin.command:
cmd: taler-merchant-dbinit
register: merchant_dbinit
changed_when: "'already initialized' not in merchant_dbinit.stdout"
failed_when: merchant_dbinit.rc not in [0]
become: true
become_user: taler-merchant
- name: Ensure taler-merchant-backend service enabled
ansible.builtin.service:
name: taler-merchant-backend
state: started
enabled: true
- name: Validate merchant configuration currency
ansible.builtin.command:
cmd: taler-config -c /etc/taler/merchant.conf -s merchant-exchange-{{ taler_config.currency | lower }} -o CURRENCY
register: merchant_config_currency
changed_when: false
- name: Assert merchant currency matches exchange currency
ansible.builtin.assert:
that:
- merchant_config_currency.stdout.strip() == taler_config.currency
fail_msg: "Merchant currency mismatch. Expected {{ taler_config.currency }}."
- name: Validate merchant exchange base URL
ansible.builtin.command:
cmd: taler-config -c /etc/taler/merchant.conf -s merchant-exchange-{{ taler_config.currency | lower }} -o EXCHANGE_BASE_URLS
register: merchant_exchange_urls
changed_when: false
- name: Assert merchant exchange URL matches configuration
ansible.builtin.assert:
that:
- merchant_exchange_urls.stdout.strip().split()[0] == "https://{{ taler_config.exchange_host }}/"
fail_msg: "Merchant exchange URL mismatch. Expected https://{{ taler_config.exchange_host }}/."

View file

@ -0,0 +1,25 @@
# Generated by Ansible - manual edits will be overwritten.
[merchant]
SERVE = UNIX
UNIXPATH = /run/taler/merchant.socket
BIND_TO = 127.0.0.1
PORT = 9966
DATABASE = postgres
UNIXPATH_MODE = 0660
{% set merchant_user = taler_config.db.merchant.user %}
{% set merchant_pass = taler_config.db.merchant.password %}
{% set merchant_auth = merchant_user if not merchant_pass else merchant_user ~ ':' ~ merchant_pass %}
[merchantdb-postgres]
CONFIG = postgres://{{ merchant_auth }}@{{ taler_config.db.host }}:{{ taler_config.db.port }}/{{ taler_config.db.merchant.name }}
[merchant-exchange-{{ taler_config.currency | lower }}]
MASTER_KEY = {{ taler_config.exchange_master_key }}
EXCHANGE_BASE_URLS = https://{{ taler_config.exchange_host }}/
CURRENCY = {{ taler_config.currency }}
[instance-default]
MERCHANT_NAME = Demogeld Shop
PAYTO_URI = payto://x-taler-merchant/{{ taler_config.merchant_host | default(taler_config.exchange_host) }}/demogeld
DEFAULT_WIRE_TRANSFER_DELAY = 1 day
DEFAULT_MAX_DEPOSIT_FEE = 0

View file

@ -0,0 +1,4 @@
---
- name: Update apt cache for Taler repo
ansible.builtin.apt:
update_cache: true

View file

@ -0,0 +1,14 @@
- name: Download GNU Taler archive key
ansible.builtin.get_url:
url: "{{ taler_config.package_repo }}taler-archive-keyring.gpg"
dest: /usr/share/keyrings/taler-archive-keyring.gpg
mode: "0644"
owner: root
group: root
- name: Add GNU Taler apt repository
ansible.builtin.apt_repository:
repo: "deb [signed-by=/usr/share/keyrings/taler-archive-keyring.gpg] {{ taler_config.package_repo }} {{ taler_config.distribution }} main"
filename: taler
state: present
notify: Update apt cache for Taler repo