binary deployment of twirre.me rust rewrite

This commit is contained in:
Twirre Meulenbelt
2026-06-28 10:08:18 +02:00
parent b44ff6f808
commit 0b65db0e50
8 changed files with 320 additions and 10 deletions

View File

@@ -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/<version>/`; `/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.

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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 }}"

View File

@@ -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 %}

View File

@@ -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

View File

@@ -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