From be7f2153802a482e6b5907ec99b8202d9d91a941 Mon Sep 17 00:00:00 2001 From: il Date: Wed, 6 May 2026 15:39:30 +0900 Subject: [PATCH] feat(ezbookkeeping): release ezbookkeeping deployment notes: - use ezbookkeeping for budget - compare to actual budget - it has no RBAC and sharing budget, try to sure (we-promise/sure) --- ansible/inventory/group_vars/all.yaml | 12 +++- ansible/playbooks/app/site.yaml | 8 +++ ansible/roles/app/handlers/main.yaml | 11 ++++ .../app/tasks/services/set_ezbookkeeping.yaml | 58 ++++++++++++++++++ .../infra/tasks/services/set_postgresql.yaml | 1 + config/secrets/secrets.yaml | 13 +++- .../ezbookkeeping/ezbookkeeping.container.j2 | 61 +++++++++++++++++++ .../auth/authelia/config/authelia.yaml.j2 | 21 +++++++ .../common/caddy/etc/app/Caddyfile.j2 | 6 ++ .../common/caddy/etc/auth/Caddyfile.j2 | 9 +++ docs/services/app/ezbookkeeping.md | 35 +++++++++++ docs/specifications/environments.md | 12 ++-- 12 files changed, 238 insertions(+), 9 deletions(-) create mode 100644 ansible/roles/app/tasks/services/set_ezbookkeeping.yaml create mode 100644 config/services/containers/app/ezbookkeeping/ezbookkeeping.container.j2 create mode 100644 docs/services/app/ezbookkeeping.md diff --git a/ansible/inventory/group_vars/all.yaml b/ansible/inventory/group_vars/all.yaml index b37d864..cf79b51 100644 --- a/ansible/inventory/group_vars/all.yaml +++ b/ansible/inventory/group_vars/all.yaml @@ -111,8 +111,8 @@ services: http: "3003" actualbudget: domain: - public: "budget" - internal: "budget.app" + public: "actualbudget" + internal: "actualbudget.app" ports: http: "5006" subuid: "101000" @@ -163,6 +163,13 @@ services: ports: http: "9980" subuid: "101000" + ezbookkeeping: + domain: + public: "budget" + internal: "budget.app" + ports: + http: "8003" + subuid: "100999" version: packages: @@ -201,3 +208,4 @@ version: affine: "0.26.3" nextcloud: "33.0.3" collabora: "25.04.9.4.1" + ezbookkeeping: "1.4.0" diff --git a/ansible/playbooks/app/site.yaml b/ansible/playbooks/app/site.yaml index 8fb42a2..a0251bb 100644 --- a/ansible/playbooks/app/site.yaml +++ b/ansible/playbooks/app/site.yaml @@ -241,6 +241,14 @@ tags: ["site", "collabora"] tags: ["site", "collabora"] + - name: Set ezbookkeeping + ansible.builtin.include_role: + name: "app" + tasks_from: "services/set_ezbookkeeping" + apply: + tags: ["site", "ezbookkeeping"] + tags: ["site", "ezbookkeeping"] + - name: Flush handlers right now ansible.builtin.meta: "flush_handlers" diff --git a/ansible/roles/app/handlers/main.yaml b/ansible/roles/app/handlers/main.yaml index a98f7cd..ef2573c 100644 --- a/ansible/roles/app/handlers/main.yaml +++ b/ansible/roles/app/handlers/main.yaml @@ -122,3 +122,14 @@ changed_when: false listen: "notification_restart_collabora" ignore_errors: true # noqa: ignore-errors + +- name: Restart ezbookkeeping + ansible.builtin.systemd: + name: "ezbookkeeping.service" + state: "restarted" + enabled: true + scope: "user" + daemon_reload: true + changed_when: false + listen: "notification_restart_ezbookkeeping" + ignore_errors: true # noqa: ignore-errors diff --git a/ansible/roles/app/tasks/services/set_ezbookkeeping.yaml b/ansible/roles/app/tasks/services/set_ezbookkeeping.yaml new file mode 100644 index 0000000..019c99c --- /dev/null +++ b/ansible/roles/app/tasks/services/set_ezbookkeeping.yaml @@ -0,0 +1,58 @@ +--- +- name: Create ezbookkeeping directory + ansible.builtin.file: + path: "{{ node['home_path'] }}/{{ item }}" + state: "directory" + owner: "{{ services['ezbookkeeping']['subuid'] }}" + group: "svadmins" + mode: "0770" + loop: + - "data/containers/ezbookkeeping" + - "data/containers/ezbookkeeping/data" + - "containers/ezbookkeeping" + - "containers/ezbookkeeping/ssl" + become: true + + +- name: Deploy root certificate + ansible.builtin.copy: + content: | + {{ hostvars['console']['ca']['root']['crt'] }} + dest: "{{ node['home_path'] }}/containers/ezbookkeeping/ssl/{{ root_cert_filename }}" + owner: "{{ services['ezbookkeeping']['subuid'] }}" + group: "svadmins" + mode: "0440" + become: true + notify: "notification_restart_ezbookkeeping" + no_log: true + +- name: Register secret value to podman secret + containers.podman.podman_secret: + name: "{{ item.name }}" + data: "{{ item.value }}" + state: "present" + force: true + loop: + - name: "EBK_AUTH_OAUTH2_CLIENT_SECRET" + value: "{{ hostvars['console']['ezbookkeeping']['oidc']['secret'] }}" + - name: "EBK_DATABASE_PASSWD" + value: "{{ hostvars['console']['postgresql']['password']['ezbookkeeping'] }}" + notify: "notification_restart_ezbookkeeping" + no_log: true + +- name: Deploy ezbookkeeping.container file + ansible.builtin.template: + src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/app/ezbookkeeping/ezbookkeeping.container.j2" + dest: "{{ node['home_path'] }}/.config/containers/systemd/ezbookkeeping.container" + owner: "{{ ansible_user }}" + group: "svadmins" + mode: "0644" + notify: "notification_restart_ezbookkeeping" + +- name: Enable ezbookkeeping.service + ansible.builtin.systemd: + name: "ezbookkeeping.service" + state: "started" + enabled: true + daemon_reload: true + scope: "user" diff --git a/ansible/roles/infra/tasks/services/set_postgresql.yaml b/ansible/roles/infra/tasks/services/set_postgresql.yaml index 5b497af..7b74d63 100644 --- a/ansible/roles/infra/tasks/services/set_postgresql.yaml +++ b/ansible/roles/infra/tasks/services/set_postgresql.yaml @@ -12,6 +12,7 @@ - "vikunja" - "affine" - "nextcloud" + - "ezbookkeeping" - name: Create postgresql directory ansible.builtin.file: diff --git a/config/secrets/secrets.yaml b/config/secrets/secrets.yaml index a96de09..bac7036 100644 --- a/config/secrets/secrets.yaml +++ b/config/secrets/secrets.yaml @@ -120,6 +120,7 @@ postgresql: vikunja: ENC[AES256_GCM,data:/+wQdoFPTBG2elI9kZbAVWrHZ0DhMaYr4dc+2z9QNdb3TcDS2PEia0JuSAg=,iv:MViZTyUD8YqMmxSTWCQpJ30f/KQdQGOzPlRHHsQ8lAw=,tag:zov3POno139dkMxFDpj2gg==,type:str] affine: ENC[AES256_GCM,data:XPXrcszsV06YqCJZ7CDqc4rCwqqNlbtLCFYfLAQ8jamLtft8L2UVrMA4WZo=,iv:vrWdBeckxB9tmEE628j4jhU+hSpE6TXYMGt0hh1Cg84=,tag:hlWwWUGht8NqWTZREMsa1Q==,type:str] nextcloud: ENC[AES256_GCM,data:ROsximNuWYMTZktmLJPx7W1Qol/uT+APgwoCtFO/6ZYYc3KxKvlk344eqEc=,iv:4d+MrfIHjJKAcwhvZ3g4go66uZcieuL7lngKErJd+fg=,tag:QbWOtxeCbiu62GyrE2atXg==,type:str] + ezbookkeeping: ENC[AES256_GCM,data:CYYQ5DVr8Na46QduvUNF6d0XBVSXTml34q3/PhIYIvUNviOVgCjqXA4wN7g=,iv:qRljohJ+wI50XxSgMElKp65HyV3mKRTqDGjw9C1S0d0=,tag:PClp7PRmC0+PV0SzZpJqqQ==,type:str] #ENC[AES256_GCM,data:ODXFUxxxdQ==,iv:s9zJVx6wo6x517tbNvC+FZ0dFzqbjqeLI6rXBq72hQA=,tag:bXoV2I3LbpmQyddJrtS3Qg==,type:comment] # # @@ -266,6 +267,14 @@ nextcloud: #ENC[AES256_GCM,data:ODXFUxxxdQ==,iv:s9zJVx6wo6x517tbNvC+FZ0dFzqbjqeLI6rXBq72hQA=,tag:bXoV2I3LbpmQyddJrtS3Qg==,type:comment] # # +#ENC[AES256_GCM,data:tMahvC9OLW4+AGLyx68SNsOPBezApw==,iv:WHx8ruuQ33J/8XtwyhvDy2cKqE7lAWvj/r5AUhdyssU=,tag:uRwheXUxqNSIhcPqGeMNog==,type:comment] +ezbookkeeping: + oidc: + secret: ENC[AES256_GCM,data:ZMIfRwXDT1ujGKoc7DGvc8/O+ciB+kajo9yOwVsMsbEjl6D8gl6I0Lbsta8=,iv:++p1TTW6gDUEvh56SjMgldrpob/VWNtiYGo6wNS8cz0=,tag:LQaW333UskiN4mtIjUAguA==,type:str] + hash: ENC[AES256_GCM,data:XyB1N3MUzBHWHAumat7/ASy/Aja/gLKmeTriOqLnMgZ9lBE1birYtFW+R0wZ+vyx79tHKVnRxzrWsxoD5jitCmHyMVrJmJKl5c4SYMhytKfBPgrNe3twcc06U+wONmgAuVpaEQlnnyzAz42SpOHbT55GegHjYzT5hXax8eRvdM6xJSY=,iv:R4+EdQuKo2JumY3cu8KPpeFezcLhlehXBxr2wVG5wHk=,tag:hpDX1x9NCCutUsnDKEf1Sg==,type:str] +#ENC[AES256_GCM,data:ODXFUxxxdQ==,iv:s9zJVx6wo6x517tbNvC+FZ0dFzqbjqeLI6rXBq72hQA=,tag:bXoV2I3LbpmQyddJrtS3Qg==,type:comment] +# +# #ENC[AES256_GCM,data:T4Wtn49AAxPd2QUFTR+q,iv:bH5goGWBDqumAat9dUv2OwfCUJUpuVqncTMqMBZUXhI=,tag:G+W6hHA+yftQ+4RJpXrxHg==,type:comment] switch: password: ENC[AES256_GCM,data:qu0f9L7A0eFq/UCpaRs=,iv:W8LLOp3MSfd/+EfNEZNf91K8GgI5eUfVPoWTRES2C0Y=,tag:Q5FlAOfwqwJwPvd7k6i+0g==,type:str] @@ -295,7 +304,7 @@ sops: UmliaFNxVTBqRkI1QWJpWGpTRWxETW8KEY/8AfU73UOzCGhny1cNnd5dCNv7bHXt k+uyWPPi+enFkVaceSwMFrA66uaWWrwAj11sXEB7yzvGFPrnAGezjQ== -----END AGE ENCRYPTED FILE----- - lastmodified: "2026-05-02T04:55:25Z" - mac: ENC[AES256_GCM,data:4U/SGYS9eNRgRvUEvZh9E0JSctkZzSpdoUYEAbnOVyU+5u8NcG9lbMUAB4kFXb9kHVGBUI5wMwnzg102g96q1IYw5m/k4lrpePceGVNAxxKpWTnkLROhJlL3Z/Bylgq2mj7PVDcGCGEB0xPDgN+ffa7ldCxIikYmSKktISguwYU=,iv:zqS9iJ54FIaNQhnfOl4YY9QcaZLbPekTxlY1AEp3m/s=,tag:TckIRAKRVyxf/UD+jejNng==,type:str] + lastmodified: "2026-05-06T05:55:57Z" + mac: ENC[AES256_GCM,data:ZIZKCze9wNuUj3C1ghHjCgVtwU9AyZPZZ0HLfEmrnPvVjyNtTlrlmj3rwR+luHSRC6kUMKWBL1NETcs3cUupcep52Ec1cxK2gHjYFK4Ri+Ep7WJB2PXzLxtxCgurrASmW1HvEihbEEpfcLA2PP4SZ7bUrjKfIPjXxcSOOKLu7kI=,iv:5evst9V+IwsB6QXNbZfKOBrUHJzI83Z4kri+P/5Xx3M=,tag:UVwMuDLQ0e/5wX8v3+L8Gg==,type:str] unencrypted_suffix: _unencrypted version: 3.12.1 diff --git a/config/services/containers/app/ezbookkeeping/ezbookkeeping.container.j2 b/config/services/containers/app/ezbookkeeping/ezbookkeeping.container.j2 new file mode 100644 index 0000000..9a319ee --- /dev/null +++ b/config/services/containers/app/ezbookkeeping/ezbookkeeping.container.j2 @@ -0,0 +1,61 @@ +[Quadlet] +DefaultDependencies=false + +[Unit] +Description=ezBookkeeping + +After=network-online.target +Wants=network-online.target + +[Container] +Image=docker.io/mayswind/ezbookkeeping:{{ version['containers']['ezbookkeeping'] }} +ContainerName=ezbookkeeping +HostName=ezbookkeeping + +PublishPort={{ services['ezbookkeeping']['ports']['http'] }}:8080/tcp + +Volume=%h/data/containers/ezbookkeeping/data:/data:rw +Volume=%h/containers/ezbookkeeping/ssl:/etc/ssl/ezbookkeeping:ro + +# General +Environment="TZ=Asia/Seoul" +Environment="EBK_SERVER_DOMAIN={{ services['ezbookkeeping']['domain']['public'] }}.{{ domain['public'] }}" +Environment="EBK_SERVER_ROOT_URL=https://{{ services['ezbookkeeping']['domain']['public'] }}.{{ domain['public'] }}/" +Environment="EBK_LOG_MODE=console" + +# Database +Environment="EBK_DATABASE_TYPE=postgres" +Environment="EBK_DATABASE_HOST={{ services['postgresql']['domain'] }}.{{ domain['internal'] }}:{{ services['postgresql']['ports']['tcp'] }}" +Environment="EBK_DATABASE_NAME=ezbookkeeping_db" +Environment="EBK_DATABASE_USER=ezbookkeeping" +Secret=EBK_DATABASE_PASSWD,type=env +Environment="EBK_DATABASE_SSL_MODE=verify-full" +Environment="PGSSLROOTCERT=/etc/ssl/ezbookkeeping/{{ root_cert_filename }}" + +# OIDC +Environment="EBK_AUTH_ENABLE_OAUTH2_AUTH=true" +Environment="EBK_AUTH_OAUTH2_PROVIDER=oidc" +Environment="EBK_AUTH_OAUTH2_CLIENT_ID=ezbookkeeping" +Secret=EBK_AUTH_OAUTH2_CLIENT_SECRET,type=env +Environment="EBK_AUTH_OAUTH2_USE_PKCE=true" +Environment="EBK_AUTH_OIDC_PROVIDER_BASE_URL=https://{{ services['authelia']['domain'] }}.{{ domain['public'] }}" +Environment="EBK_AUTH_ENABLE_OIDC_DISPLAY_NAME=true" +Environment="EBK_AUTH_OIDC_CUSTOM_DISPLAY_NAME=Authelia" + +# Registration / auth policy +Environment="EBK_AUTH_ENABLE_INTERNAL_AUTH=false" +Environment="EBK_USER_ENABLE_REGISTER=true" +Environment="EBK_AUTH_OAUTH2_AUTO_REGISTER=true" + +# AI / MCP disabled by default +Environment="EBK_MCP_ENABLE_MCP=false" +Environment="EBK_LLM_TRANSACTION_FROM_AI_IMAGE_RECOGNITION=false" + +[Service] +ExecStartPre=/usr/bin/nc -zv {{ services['postgresql']['domain'] }}.{{ domain['internal'] }} {{ services['postgresql']['ports']['tcp'] }} +Restart=always +RestartSec=10s +TimeoutStopSec=120 + +[Install] +WantedBy=default.target \ No newline at end of file diff --git a/config/services/containers/auth/authelia/config/authelia.yaml.j2 b/config/services/containers/auth/authelia/config/authelia.yaml.j2 index bab02f4..de431c3 100644 --- a/config/services/containers/auth/authelia/config/authelia.yaml.j2 +++ b/config/services/containers/auth/authelia/config/authelia.yaml.j2 @@ -387,3 +387,24 @@ identity_providers: access_token_signed_response_alg: 'none' userinfo_signed_response_alg: 'none' token_endpoint_auth_method: 'client_secret_post' + # https://www.authelia.com/integration/openid-connect/clients/ezbookkeeping/ + - client_id: 'ezbookkeeping' + client_name: 'ezBookkeeping' + client_secret: '{{ hostvars['console']['ezbookkeeping']['oidc']['hash'] }}' + public: false + authorization_policy: 'one_factor' + require_pkce: true + pkce_challenge_method: 'S256' + redirect_uris: + - 'https://budget.ilnmors.com/oauth2/callback' + scopes: + - 'openid' + - 'profile' + - 'email' + response_types: + - 'code' + grant_types: + - 'authorization_code' + access_token_signed_response_alg: 'none' + userinfo_signed_response_alg: 'none' + token_endpoint_auth_method: 'client_secret_basic' diff --git a/config/services/containers/common/caddy/etc/app/Caddyfile.j2 b/config/services/containers/common/caddy/etc/app/Caddyfile.j2 index 1e0cea2..e7d6fa8 100644 --- a/config/services/containers/common/caddy/etc/app/Caddyfile.j2 +++ b/config/services/containers/common/caddy/etc/app/Caddyfile.j2 @@ -89,3 +89,9 @@ header_up Host {http.request.header.X-Forwarded-Host} } } +{{ services['ezbookkeeping']['domain']['internal'] }}.{{ domain['internal'] }} { + import private_tls + reverse_proxy host.containers.internal:{{ services['ezbookkeeping']['ports']['http'] }} { + header_up Host {http.request.header.X-Forwarded-Host} + } +} diff --git a/config/services/containers/common/caddy/etc/auth/Caddyfile.j2 b/config/services/containers/common/caddy/etc/auth/Caddyfile.j2 index 708930b..c90357a 100644 --- a/config/services/containers/common/caddy/etc/auth/Caddyfile.j2 +++ b/config/services/containers/common/caddy/etc/auth/Caddyfile.j2 @@ -154,6 +154,15 @@ } } } +{{ services['ezbookkeeping']['domain']['public'] }}.{{ domain['public'] }} { + import crowdsec_log + route { + crowdsec + reverse_proxy https://{{services['ezbookkeeping']['domain']['internal'] }}.{{ domain['internal'] }} { + header_up Host {http.reverse_proxy.upstream.host} + } + } +} # Internal domain {{ node['name'] }}.{{ domain['internal'] }} { diff --git a/docs/services/app/ezbookkeeping.md b/docs/services/app/ezbookkeeping.md new file mode 100644 index 0000000..b7b0f13 --- /dev/null +++ b/docs/services/app/ezbookkeeping.md @@ -0,0 +1,35 @@ +# ezBookkeeping + +## Prerequisite + +### Create database + +- Create the password with `openssl rand -base64 32` + - Save this value in secrets.yaml in `postgresql.password.ezbookkeeping` + - Access infra server to create paperless_db with `podman exec -it postgresql psql -U postgres` + +```SQL +CREATE USER ezbookkeeping WITH PASSWORD 'postgresql.password.ezbookkeeping'; +CREATE DATABASE ezbookkeeping_db; +ALTER DATABASE ezbookkeeping_db OWNER TO ezbookkeeping; +``` + +### Create oidc secret and hash + +- Create the secret with `openssl rand -base64 32` +- access to auth vm + - `podman exec -it authelia sh` + - `authelia crypto hash generate pbkdf2 --password 'ezbookkeeping.oidc.secret'` +- Save this value in secrets.yaml in `ezbookkeeping.oidc.secret` and `ezbookkeeping.oidc.hash` + +### Add postgresql dump backup list + +- [set_postgresql.yaml](../../../ansible/roles/infra/tasks/services/set_postgresql.yaml) + +```yaml +- name: Set connected services list + ansible.builtin.set_fact: + connected_services: + - ... + - "ezbookkeeping" +``` diff --git a/docs/specifications/environments.md b/docs/specifications/environments.md index 004afb9..cbdefc7 100644 --- a/docs/specifications/environments.md +++ b/docs/specifications/environments.md @@ -117,16 +117,18 @@ - [x] Vaultwarden - [x] Gitea - [x] Immich - - [x] Actual budget + - [x] Actual budget (Comparing to ezBookkeeping) - [x] Paperless-ngx - - [x] vikunja - - [x] OpenCloud + - [x] vikunja (Comparing to Nextcloud deck) + - [x] OpenCloud (Comparing to Nextcloud) - [x] affine \(Notion substitution\) - [x] Nextcloud \(Use nextcloud as CalDAV and CardDav, kanban and todo\) - [x] Collabora office \(Link to Nextcloud, it works well\) + - [x] ezBookkeeping (budget, consider this the main budget app instead of actual budget) + - use budget.ilnmors.com for ezBookkeeping, actual budget domain is changed as actualbudget.ilnmors.com - WriteFreely - - MediaCMS - - Funkwhale + - MediaCMS or PeerTube + - Funkwhale or Navidrome or Jellyfin - Kavita - Audiobookshelf - Miniflux