From 0b65db0e50d223282a476fcef7cae9e3962f99be Mon Sep 17 00:00:00 2001 From: Twirre Meulenbelt <43213592+TwirreM@users.noreply.github.com> Date: Sun, 28 Jun 2026 10:08:18 +0200 Subject: [PATCH] binary deployment of twirre.me rust rewrite --- README.md | 8 +- group_vars/all/main.yml | 16 +- group_vars/all/vault.example.yml | 3 +- roles/binary_app/tasks/app.yml | 254 ++++++++++++++++++ roles/binary_app/tasks/main.yml | 7 + roles/binary_app/templates/binary-app.env.j2 | 7 + .../templates/binary-app.service.j2 | 17 ++ site.yml | 18 ++ 8 files changed, 320 insertions(+), 10 deletions(-) create mode 100644 roles/binary_app/tasks/app.yml create mode 100644 roles/binary_app/tasks/main.yml create mode 100644 roles/binary_app/templates/binary-app.env.j2 create mode 100644 roles/binary_app/templates/binary-app.service.j2 diff --git a/README.md b/README.md index 8152174..ab83de0 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,8 @@ Portable Ansible provisioning for Twirre infrastructure. The current layout uses - A dedicated `backupagent` user for rsync-based backups with restricted SSH settings and passwordless `sudo /usr/bin/rsync` - Docker from distro packages - Bun installed to `/opt/bun` with `/usr/local/bin/bun` -- Two Bun app services: `twirre.io` and `twirre.me` +- The `twirre.io` Bun app service +- The `twirre.me` Rust binary service from a GitHub Release asset - Gitea and Docker Mailserver as Compose stacks under `/srv` - nginx virtual hosts for the retained public domains - apt-based certbot with ACME webroot support @@ -43,7 +44,7 @@ Update the inventory and variables first, especially: - `inventory/hosts.yml` - `group_vars/all/main.yml` - `group_vars/all/vault.yml` -- repository URLs and domains for the Bun apps +- repository URLs, release versions, and domains for the app services - `backupagent.authorized_keys` Run a syntax check: @@ -61,7 +62,7 @@ uv run ansible-playbook site.yml --ask-vault-pass ## Result - `twirre.io`: Bun source checkout in `/srv/twirre/twirre.io`; runtime config in `/etc/twirre-io/app.env`; data in `/var/lib/twirre-io/app.sqlite3`, public files in `/var/lib/twirre-io/files`, and hidden files in `/var/lib/twirre-io/hfiles`; systemd unit `twirre_io.service`. -- `twirre.me`: Bun source checkout in `/srv/twirre/twirre.me`; runtime config in `/etc/twirre-me/app.env`; data directory reserved at `/var/lib/twirre-me`; systemd unit `twirre_me.service`. +- `twirre.me`: Rust release archive downloaded from GitHub Releases into `/opt/twirre-me/releases//`; `/opt/twirre-me/current` points at the selected version; runtime config in `/etc/twirre-me/app.env`; data directory reserved at `/var/lib/twirre-me`; systemd unit `twirre_me.service`. - `Gitea`: Docker Compose stack in `/srv/gitea/compose.yaml`; persistent application data in `/srv/gitea/data`; systemd unit `gitea-compose.service`; public HTTP behind nginx at `git.twirre.io`, SSH on port `2222`. - `Mailserver`: Docker Compose stack in `/srv/mail/compose.yaml` with environment in `/srv/mail/mailserver.env`; mail data, state, logs, and docker-mailserver config under `/srv/mail/docker-data/dms/`; Let's Encrypt certificates mounted from `/etc/letsencrypt`; systemd unit `mailserver-compose.service`. - `lagrange.meulenbelt.nl`: static placeholder site rooted at `/srv/lagrange`. @@ -71,6 +72,7 @@ uv run ansible-playbook site.yml --ask-vault-pass ## Notes - nginx falls back to snakeoil certificates until a matching ACME certificate already exists on disk. +- `twirre.me` deploys the version declared in `binary_apps`; update `version` after publishing a matching GitHub Release asset and checksum. - A run with `certbot_manage_certificates: true` requests certificates during that playbook run. If DNS and HTTP reachability are correct, issuance happens immediately. - `mailserver` starts only after its live Let's Encrypt certificate exists on disk, so the same run that issues `mail.twirre.io` can also bring up Docker Mailserver. - ACME issuance is disabled by default through `certbot_manage_certificates: false` so the first provisioning run can complete before DNS and public reachability are finalized. diff --git a/group_vars/all/main.yml b/group_vars/all/main.yml index 5c21208..aa95d2e 100644 --- a/group_vars/all/main.yml +++ b/group_vars/all/main.yml @@ -15,6 +15,7 @@ base_packages_common: docker_enabled: true bun_enabled: true +binary_apps_enabled: true nginx_enabled: true certbot_enabled: true wireguard_enabled: true @@ -187,16 +188,21 @@ bun_apps: extra_directories: - path: "{{ twirre_io_files.visible_dir }}" - path: "{{ twirre_io_files.hidden_dir }}" + +binary_apps: - name: twirre-me - repo: git@github.com:TwirreM/twirre.me.git - version: main + github_repo: TwirreM/twirre.me + version: v2.0.0 + asset_name: twirre-me-linux-x86_64.tar.gz + checksum_asset_name: twirre-me-linux-x86_64.tar.gz.sha256 + binary_name: twirre-me deploy_user: twirre-me deploy_group: twirre-me - path: /srv/twirre/twirre.me + install_root: /opt/twirre-me service_name: twirre_me - entrypoint: index.ts port: 13013 - git_ssh_key: "{{ vault_twirre_me_deploy_key | default('') }}" + remove_legacy_deploy_key: true + github_token: "{{ vault_twirre_me_release_token | default('') }}" env: PORT: "13013" diff --git a/group_vars/all/vault.example.yml b/group_vars/all/vault.example.yml index 1313a09..e88b19d 100644 --- a/group_vars/all/vault.example.yml +++ b/group_vars/all/vault.example.yml @@ -28,5 +28,4 @@ vault_twirre_io_env: {} vault_twirre_me_env: {} vault_twirre_io_deploy_key: | REPLACE_ME -vault_twirre_me_deploy_key: | - REPLACE_ME +vault_twirre_me_release_token: REPLACE_ME diff --git a/roles/binary_app/tasks/app.yml b/roles/binary_app/tasks/app.yml new file mode 100644 index 0000000..e044219 --- /dev/null +++ b/roles/binary_app/tasks/app.yml @@ -0,0 +1,254 @@ +--- +- name: Set binary app derived values + ansible.builtin.set_fact: + binary_app_install_root: "{{ binary_app.install_root | default('/opt/' ~ binary_app.name) }}" + binary_app_release_dir: "{{ binary_app.install_root | default('/opt/' ~ binary_app.name) }}/releases/{{ binary_app.version }}" + binary_app_current_path: "{{ binary_app.install_root | default('/opt/' ~ binary_app.name) }}/current" + binary_app_checksum_asset_name: "{{ binary_app.checksum_asset_name | default(binary_app.asset_name ~ '.sha256') }}" + binary_app_binary_path: "{{ binary_app.binary_path | default(binary_app.binary_name) }}" + +- name: Ensure binary app group exists + ansible.builtin.group: + name: "{{ binary_app.deploy_group }}" + state: present + +- name: Ensure binary app user exists + ansible.builtin.user: + name: "{{ binary_app.deploy_user }}" + group: "{{ binary_app.deploy_group }}" + system: true + shell: /usr/sbin/nologin + create_home: true + +- name: Ensure binary app directories exist + ansible.builtin.file: + path: "{{ item.path }}" + state: directory + owner: "{{ item.owner }}" + group: "{{ item.group }}" + mode: "{{ item.mode }}" + loop: + - path: "{{ binary_app_install_root }}" + owner: root + group: root + mode: "0755" + - path: "{{ binary_app_install_root }}/releases" + owner: root + group: root + mode: "0755" + - path: "{{ binary_app_release_dir }}" + owner: root + group: root + mode: "0755" + - path: "/var/lib/{{ binary_app.name }}" + owner: "{{ binary_app.deploy_user }}" + group: "{{ binary_app.deploy_group }}" + mode: "0755" + - path: "/etc/{{ binary_app.name }}" + owner: root + group: "{{ binary_app.deploy_group }}" + mode: "0750" + loop_control: + label: "{{ item.path }}" + +- name: Ensure binary app extra directories exist + ansible.builtin.file: + path: "{{ item.path }}" + state: directory + owner: "{{ item.owner | default(binary_app.deploy_user) }}" + group: "{{ item.group | default(binary_app.deploy_group) }}" + mode: "{{ item.mode | default('0755') }}" + loop: "{{ binary_app.extra_directories | default([]) }}" + loop_control: + label: "{{ item.path }}" + +- name: Remove legacy Bun deploy key + ansible.builtin.file: + path: "/etc/{{ binary_app.name }}/deploy_key" + state: absent + when: binary_app.remove_legacy_deploy_key | default(false) | bool + +- name: Look up binary app GitHub release + ansible.builtin.uri: + url: "https://api.github.com/repos/{{ binary_app.github_repo }}/releases/tags/{{ binary_app.version }}" + headers: >- + {{ + { + 'Accept': 'application/vnd.github+json', + 'X-GitHub-Api-Version': '2022-11-28' + } + | combine( + {'Authorization': 'Bearer ' ~ binary_app.github_token} + if (binary_app.github_token | default('') | length > 0) + else {} + ) + }} + return_content: true + status_code: 200 + register: binary_app_github_release + no_log: "{{ binary_app.github_token | default('') | length > 0 }}" + +- name: Find binary app GitHub release assets + ansible.builtin.set_fact: + binary_app_asset_matches: "{{ binary_app_github_release.json.assets | selectattr('name', 'equalto', binary_app.asset_name) | list }}" + binary_app_checksum_asset_matches: "{{ binary_app_github_release.json.assets | selectattr('name', 'equalto', binary_app_checksum_asset_name) | list }}" + +- name: Validate binary app GitHub release assets + ansible.builtin.assert: + that: + - binary_app_asset_matches | length == 1 + - binary_app_checksum_asset_matches | length == 1 + fail_msg: "Release {{ binary_app.version }} for {{ binary_app.github_repo }} must contain {{ binary_app.asset_name }} and {{ binary_app_checksum_asset_name }}." + +- name: Set binary app GitHub release asset API URLs + ansible.builtin.set_fact: + binary_app_asset_api_url: "{{ binary_app_asset_matches[0].url }}" + binary_app_checksum_asset_api_url: "{{ binary_app_checksum_asset_matches[0].url }}" + +- name: Download binary app checksum + ansible.builtin.command: + argv: >- + {{ + [ + 'curl', + '--fail', + '--location', + '--silent', + '--show-error', + '--header', + 'Accept: application/octet-stream', + '--header', + 'X-GitHub-Api-Version: 2022-11-28' + ] + + ( + ['--header', 'Authorization: Bearer ' ~ binary_app.github_token] + if (binary_app.github_token | default('') | length > 0) + else [] + ) + + [ + '--output', + binary_app_release_dir ~ '/' ~ binary_app_checksum_asset_name, + binary_app_checksum_asset_api_url + ] + }} + creates: "{{ binary_app_release_dir }}/{{ binary_app_checksum_asset_name }}" + no_log: "{{ binary_app.github_token | default('') | length > 0 }}" + +- name: Ensure binary app checksum file permissions + ansible.builtin.file: + path: "{{ binary_app_release_dir }}/{{ binary_app_checksum_asset_name }}" + owner: root + group: root + mode: "0644" + +- name: Download binary app release archive + ansible.builtin.command: + argv: >- + {{ + [ + 'curl', + '--fail', + '--location', + '--silent', + '--show-error', + '--header', + 'Accept: application/octet-stream', + '--header', + 'X-GitHub-Api-Version: 2022-11-28' + ] + + ( + ['--header', 'Authorization: Bearer ' ~ binary_app.github_token] + if (binary_app.github_token | default('') | length > 0) + else [] + ) + + [ + '--output', + binary_app_release_dir ~ '/' ~ binary_app.asset_name, + binary_app_asset_api_url + ] + }} + creates: "{{ binary_app_release_dir }}/{{ binary_app.asset_name }}" + register: binary_app_archive + no_log: "{{ binary_app.github_token | default('') | length > 0 }}" + +- name: Ensure binary app release archive permissions + ansible.builtin.file: + path: "{{ binary_app_release_dir }}/{{ binary_app.asset_name }}" + owner: root + group: root + mode: "0644" + +- name: Verify binary app release archive checksum + ansible.builtin.command: + argv: + - sha256sum + - -c + - "{{ binary_app_checksum_asset_name }}" + chdir: "{{ binary_app_release_dir }}" + changed_when: false + +- name: Unpack binary app release archive + ansible.builtin.unarchive: + src: "{{ binary_app_release_dir }}/{{ binary_app.asset_name }}" + dest: "{{ binary_app_release_dir }}" + remote_src: true + owner: root + group: root + mode: "0755" + creates: "{{ binary_app_release_dir }}/{{ binary_app_binary_path }}" + register: binary_app_unpack + +- name: Ensure binary app executable permissions + ansible.builtin.file: + path: "{{ binary_app_release_dir }}/{{ binary_app_binary_path }}" + owner: root + group: root + mode: "0755" + +- name: Point binary app current symlink at selected release + ansible.builtin.file: + src: "{{ binary_app_release_dir }}" + dest: "{{ binary_app_current_path }}" + state: link + force: true + register: binary_app_current + +- name: Render binary app environment file + ansible.builtin.template: + src: binary-app.env.j2 + dest: "/etc/{{ binary_app.name }}/app.env" + owner: root + group: "{{ binary_app.deploy_group }}" + mode: "0640" + register: binary_app_env + +- name: Install binary app systemd unit + ansible.builtin.template: + src: binary-app.service.j2 + dest: "/etc/systemd/system/{{ binary_app.service_name }}.service" + owner: root + group: root + mode: "0644" + register: binary_app_unit + +- name: Reload systemd for binary app changes + ansible.builtin.systemd_service: + daemon_reload: true + when: binary_app_unit.changed + +- name: Ensure binary app service is enabled and running + ansible.builtin.service: + name: "{{ binary_app.service_name }}" + state: >- + {{ + 'restarted' + if ( + binary_app_archive.changed + or binary_app_unpack.changed + or binary_app_current.changed + or binary_app_env.changed + or binary_app_unit.changed + ) + else 'started' + }} + enabled: true diff --git a/roles/binary_app/tasks/main.yml b/roles/binary_app/tasks/main.yml new file mode 100644 index 0000000..9c3a7c4 --- /dev/null +++ b/roles/binary_app/tasks/main.yml @@ -0,0 +1,7 @@ +--- +- name: Deploy configured binary applications + ansible.builtin.include_tasks: app.yml + loop: "{{ binary_apps }}" + loop_control: + loop_var: binary_app + label: "{{ binary_app.name }}" diff --git a/roles/binary_app/templates/binary-app.env.j2 b/roles/binary_app/templates/binary-app.env.j2 new file mode 100644 index 0000000..3a7f7a5 --- /dev/null +++ b/roles/binary_app/templates/binary-app.env.j2 @@ -0,0 +1,7 @@ +{% set app_vault_env_var = 'vault_' + (binary_app.name | replace('-', '_')) + '_env' %} +{% set app_vault_env = lookup('vars', app_vault_env_var, default={}) %} +{% set app_non_vault_env_keys = binary_app.non_vault_env_keys | default([]) %} +{% set app_filtered_vault_env = app_vault_env | dict2items | rejectattr('key', 'in', app_non_vault_env_keys) | items2dict %} +{% for key, value in (binary_app.env | combine(app_filtered_vault_env)) | dictsort %} +{{ key }}={{ value }} +{% endfor %} diff --git a/roles/binary_app/templates/binary-app.service.j2 b/roles/binary_app/templates/binary-app.service.j2 new file mode 100644 index 0000000..aa59e3c --- /dev/null +++ b/roles/binary_app/templates/binary-app.service.j2 @@ -0,0 +1,17 @@ +[Unit] +Description={{ binary_app.name }} binary service +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User={{ binary_app.deploy_user }} +Group={{ binary_app.deploy_group }} +WorkingDirectory={{ binary_app.working_directory | default(binary_app_current_path) }} +EnvironmentFile=/etc/{{ binary_app.name }}/app.env +ExecStart={{ binary_app_current_path }}/{{ binary_app_binary_path }} +Restart=always +RestartSec=1 + +[Install] +WantedBy=multi-user.target diff --git a/site.yml b/site.yml index 98eddcb..aebc4dc 100644 --- a/site.yml +++ b/site.yml @@ -17,6 +17,22 @@ fail_msg: Define at least one Bun application in bun_apps. when: bun_enabled | bool + - name: Validate binary application definitions + ansible.builtin.assert: + that: + - binary_apps is iterable + - binary_apps | length > 0 + - binary_apps | map(attribute='name') | select('string') | list | length == binary_apps | length + - binary_apps | map(attribute='github_repo') | select('string') | list | length == binary_apps | length + - binary_apps | map(attribute='version') | select('string') | list | length == binary_apps | length + - binary_apps | map(attribute='asset_name') | select('string') | list | length == binary_apps | length + - binary_apps | map(attribute='binary_name') | select('string') | list | length == binary_apps | length + - binary_apps | map(attribute='deploy_user') | select('string') | list | length == binary_apps | length + - binary_apps | map(attribute='deploy_group') | select('string') | list | length == binary_apps | length + - binary_apps | map(attribute='service_name') | select('string') | list | length == binary_apps | length + fail_msg: Define at least one binary application in binary_apps. + when: binary_apps_enabled | bool + - name: Validate WireGuard configuration when enabled ansible.builtin.assert: that: @@ -51,6 +67,8 @@ when: gitea_enabled | bool - role: bun_app when: bun_enabled | bool + - role: binary_app + when: binary_apps_enabled | bool - role: nginx when: nginx_enabled | bool - role: certbot