feat: add OpenApply role with provisioning, configuration, and service management for Proxmox LXC

This commit is contained in:
Nathan 2026-04-17 19:19:11 -04:00
parent a7ac8004d4
commit 46d98af51d
16 changed files with 463 additions and 1 deletions

0
.ansible/.lock Normal file
View File

View File

@ -0,0 +1,37 @@
---
# Proxmox API settings (credentials come from vault variables)
openapply_pve_api_host: "{{ vault_proxmox_api_host | default('') }}"
openapply_pve_api_user: "{{ vault_proxmox_api_user | default('') }}"
openapply_pve_api_token_id: "{{ vault_proxmox_api_token_id | default('') }}"
openapply_pve_api_token_secret: "{{ vault_proxmox_api_token_secret | default('') }}"
openapply_proxmox_validate_certs: false
# Proxmox target and LXC specification
openapply_pve_node: pve01
openapply_lxc_vmid: 105
openapply_lxc_hostname: openapply-prod
openapply_lxc_template: local:vztmpl/ubuntu-24.04-standard_24.04-1_amd64.tar.zst
openapply_lxc_storage: local-lvm
openapply_lxc_cores: 2
openapply_lxc_memory_mb: 4096
openapply_lxc_swap_mb: 512
openapply_lxc_unprivileged: true
openapply_lxc_onboot: true
openapply_lxc_features:
nesting: 1
# LXC networking
openapply_lxc_bridge: vmbr0
openapply_lxc_ip_cidr: 10.0.0.105/24
openapply_lxc_gateway: 10.0.0.2
openapply_lxc_management_ip: 10.0.0.105
openapply_lxc_ssh_user: root
openapply_lxc_ssh_port: 22
openapply_lxc_nic_firewall: true
openapply_use_proxmox_nic: false
# LXC credentials (from vault)
openapply_lxc_password: "{{ vault_openapply_lxc_root_password | default('') }}"
# Controller runtime preflight
openapply_validate_controller_python_deps: true

View File

@ -0,0 +1,5 @@
---
# Cluster-scoped overrides for Proxmox-hosted workloads
# Override these values here when multiple Proxmox nodes diverge.
openapply_proxmox_validate_certs: false

View File

@ -0,0 +1,111 @@
---
- name: Provision OpenApply LXC on Proxmox
hosts: localhost
gather_facts: false
connection: local
pre_tasks:
- name: Validate required infrastructure variables
ansible.builtin.assert:
that:
- openapply_pve_api_host | length > 0
- openapply_pve_api_user | length > 0
- openapply_pve_api_token_id | length > 0
- openapply_pve_api_token_secret | length > 0
- openapply_pve_node | length > 0
- openapply_lxc_vmid | int > 0
- openapply_lxc_hostname | length > 0
- openapply_lxc_template | length > 0
- openapply_lxc_storage | length > 0
- openapply_lxc_ip_cidr | length > 0
- openapply_lxc_gateway | length > 0
- openapply_lxc_management_ip | length > 0
- openapply_lxc_password | length > 0
fail_msg: >-
Required Proxmox/OpenApply LXC variables are missing. Check
group_vars/all/openapply.yml and vault variables.
- name: Validate Proxmox Python dependencies on controller
ansible.builtin.command: python3 -c "import proxmoxer, requests"
register: openapply_controller_python_deps
changed_when: false
failed_when: openapply_controller_python_deps.rc != 0
when: openapply_validate_controller_python_deps | bool
tasks:
- name: Ensure OpenApply LXC is present and started
community.proxmox.proxmox:
api_host: "{{ openapply_pve_api_host }}"
api_user: "{{ openapply_pve_api_user }}"
api_token_id: "{{ openapply_pve_api_token_id }}"
api_token_secret: "{{ openapply_pve_api_token_secret }}"
validate_certs: "{{ openapply_proxmox_validate_certs }}"
node: "{{ openapply_pve_node }}"
vmid: "{{ openapply_lxc_vmid }}"
hostname: "{{ openapply_lxc_hostname }}"
ostemplate: "{{ openapply_lxc_template }}"
storage: "{{ openapply_lxc_storage }}"
cores: "{{ openapply_lxc_cores }}"
memory: "{{ openapply_lxc_memory_mb }}"
swap: "{{ openapply_lxc_swap_mb }}"
password: "{{ openapply_lxc_password }}"
onboot: "{{ openapply_lxc_onboot }}"
unprivileged: "{{ openapply_lxc_unprivileged }}"
netif:
net0: "name=eth0,bridge={{ openapply_lxc_bridge }},ip={{ openapply_lxc_ip_cidr }},gw={{ openapply_lxc_gateway }}"
features: "{{ openapply_lxc_features }}"
state: started
register: openapply_lxc_status
- name: Reconcile LXC NIC configuration via Proxmox API
when: openapply_use_proxmox_nic | bool
block:
- name: Ensure net0 configuration through proxmox_nic
community.proxmox.proxmox_nic:
api_host: "{{ openapply_pve_api_host }}"
api_user: "{{ openapply_pve_api_user }}"
api_token_id: "{{ openapply_pve_api_token_id }}"
api_token_secret: "{{ openapply_pve_api_token_secret }}"
validate_certs: "{{ openapply_proxmox_validate_certs }}"
vmid: "{{ openapply_lxc_vmid }}"
name: "{{ openapply_lxc_hostname }}"
interface: net0
bridge: "{{ openapply_lxc_bridge }}"
firewall: "{{ openapply_lxc_nic_firewall }}"
state: present
rescue:
- name: Continue when proxmox_nic is unsupported for this target
ansible.builtin.debug:
msg: >-
proxmox_nic could not be applied to vmid {{ openapply_lxc_vmid }};
continuing with proxmox container network configuration only.
- name: Add OpenApply LXC to runtime inventory
ansible.builtin.add_host:
name: "{{ openapply_lxc_hostname }}"
ansible_host: "{{ openapply_lxc_management_ip }}"
ansible_user: "{{ openapply_lxc_ssh_user }}"
ansible_port: "{{ openapply_lxc_ssh_port }}"
ansible_python_interpreter: /usr/bin/python3
groups: lxc_guests
- name: Display provisioning summary
ansible.builtin.debug:
msg:
- "LXC hostname: {{ openapply_lxc_hostname }}"
- "LXC management IP: {{ openapply_lxc_management_ip }}"
- "LXC vmid: {{ openapply_lxc_vmid }}"
- "LXC changed: {{ openapply_lxc_status.changed | default(false) }}"
- name: Configure OpenApply application inside guest
hosts: lxc_guests
gather_facts: true
become: true
pre_tasks:
- name: Wait for SSH connectivity to LXC guest
ansible.builtin.wait_for_connection:
timeout: 300
roles:
- role: openapply_app

View File

@ -8,8 +8,14 @@
# Last updated: 2026-01-10 # Last updated: 2026-01-10
collections: collections:
# Community Proxmox Collection
# Used for: proxmox lifecycle, kvm, and nic management modules
# Docs: https://docs.ansible.com/ansible/latest/collections/community/proxmox/
- name: community.proxmox
version: ">=1.3.0"
# Community General Collection # Community General Collection
# Used for: proxmox modules, docker modules, general utilities # Used for: docker modules and general utilities
# Docs: https://docs.ansible.com/ansible/latest/collections/community/general/ # Docs: https://docs.ansible.com/ansible/latest/collections/community/general/
- name: community.general - name: community.general
version: ">=8.0.0" version: ">=8.0.0"

View File

@ -0,0 +1,39 @@
---
openapply_app_repo_url: https://github.com/sergeykhval/openapply.git
openapply_app_repo_version: main
openapply_app_service_name: openapply
openapply_app_service_user: openapply
openapply_app_service_group: openapply
openapply_app_root: /opt/openapply
openapply_app_service_port: 80
openapply_app_node_major: "20"
openapply_app_install_firebase_cli: true
openapply_app_enable_firewall: true
openapply_app_allowed_tcp_ports:
- 22
- 80
- 443
openapply_app_build_subdirs:
- astro
- spa
openapply_app_force_rebuild: false
openapply_app_git_ssh_key_file: ""
openapply_app_start_command: >-
pnpm --dir {{ openapply_app_root }}/astro preview --host 0.0.0.0 --port {{ openapply_app_service_port }}
openapply_app_env:
NODE_ENV: production
PORT: "{{ openapply_app_service_port }}"
openapply_app_firebase_token: "{{ vault_openapply_firebase_token | default('') }}"
openapply_app_verify_status_codes:
- 200
- 301
- 302

View File

@ -0,0 +1,9 @@
---
- name: Reload systemd
ansible.builtin.systemd:
daemon_reload: true
- name: Restart OpenApply service
ansible.builtin.systemd:
name: "{{ openapply_app_service_name }}"
state: restarted

View File

@ -0,0 +1,94 @@
---
- name: Ensure OpenApply group exists
ansible.builtin.group:
name: "{{ openapply_app_service_group }}"
state: present
- name: Ensure OpenApply service user exists
ansible.builtin.user:
name: "{{ openapply_app_service_user }}"
group: "{{ openapply_app_service_group }}"
shell: /usr/sbin/nologin
create_home: false
system: true
state: present
- name: Ensure OpenApply root directory exists
ansible.builtin.file:
path: "{{ openapply_app_root }}"
state: directory
owner: "{{ openapply_app_service_user }}"
group: "{{ openapply_app_service_group }}"
mode: "0755"
- name: Render OpenApply environment file
ansible.builtin.template:
src: openapply.env.j2
dest: "{{ openapply_app_root }}/.env"
owner: "{{ openapply_app_service_user }}"
group: "{{ openapply_app_service_group }}"
mode: "0640"
- name: Sync OpenApply source code
ansible.builtin.git:
repo: "{{ openapply_app_repo_url }}"
version: "{{ openapply_app_repo_version }}"
dest: "{{ openapply_app_root }}"
update: true
force: false
accept_hostkey: true
key_file: "{{ openapply_app_git_ssh_key_file | default(omit, true) }}"
become: true
become_user: "{{ openapply_app_service_user }}"
register: openapply_repo_checkout
- name: Check node_modules presence
ansible.builtin.stat:
path: "{{ openapply_app_root }}/node_modules"
register: openapply_node_modules
- name: Install OpenApply dependencies via pnpm
ansible.builtin.command:
cmd: pnpm install --frozen-lockfile
chdir: "{{ openapply_app_root }}"
become: true
become_user: "{{ openapply_app_service_user }}"
when:
- openapply_repo_checkout.changed or openapply_app_force_rebuild | bool or not openapply_node_modules.stat.exists
changed_when: true
- name: Check which build directories exist
ansible.builtin.stat:
path: "{{ openapply_app_root }}/{{ item }}"
register: openapply_build_dir_stats
loop: "{{ openapply_app_build_subdirs }}"
- name: Build OpenApply subprojects
ansible.builtin.command:
cmd: pnpm run build
chdir: "{{ openapply_app_root }}/{{ item.item }}"
become: true
become_user: "{{ openapply_app_service_user }}"
loop: "{{ openapply_build_dir_stats.results }}"
when:
- item.stat.exists
- openapply_repo_checkout.changed or openapply_app_force_rebuild | bool
changed_when: true
- name: Install OpenApply systemd unit
ansible.builtin.template:
src: openapply.service.j2
dest: "/etc/systemd/system/{{ openapply_app_service_name }}.service"
owner: root
group: root
mode: "0644"
notify:
- Reload systemd
- Restart OpenApply service
- name: Ensure OpenApply service is enabled and started
ansible.builtin.systemd:
name: "{{ openapply_app_service_name }}"
enabled: true
state: started
daemon_reload: true

View File

@ -0,0 +1,15 @@
---
- name: Validate role input
ansible.builtin.import_tasks: validate.yml
- name: Install base prerequisites
ansible.builtin.import_tasks: prereqs.yml
- name: Configure runtime dependencies
ansible.builtin.import_tasks: setup_env.yml
- name: Deploy and build OpenApply
ansible.builtin.import_tasks: deploy_code.yml
- name: Verify running service
ansible.builtin.import_tasks: verify.yml

View File

@ -0,0 +1,37 @@
---
- name: Install OpenApply prerequisite packages
ansible.builtin.apt:
name:
- ca-certificates
- curl
- git
- gnupg
- ufw
- build-essential
state: present
update_cache: true
cache_valid_time: 3600
- name: Configure UFW for production web access
when: openapply_app_enable_firewall | bool
block:
- name: Set default incoming firewall policy
community.general.ufw:
direction: incoming
default: deny
- name: Set default outgoing firewall policy
community.general.ufw:
direction: outgoing
default: allow
- name: Allow required TCP ports
community.general.ufw:
rule: allow
port: "{{ item }}"
proto: tcp
loop: "{{ openapply_app_allowed_tcp_ports }}"
- name: Enable UFW
community.general.ufw:
state: enabled

View File

@ -0,0 +1,42 @@
---
- name: Ensure apt keyrings directory exists
ansible.builtin.file:
path: /etc/apt/keyrings
state: directory
mode: "0755"
- name: Download NodeSource signing key
ansible.builtin.get_url:
url: https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key
dest: /tmp/nodesource.gpg.key
mode: "0644"
- name: Install NodeSource keyring
ansible.builtin.command:
cmd: gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg /tmp/nodesource.gpg.key
creates: /etc/apt/keyrings/nodesource.gpg
- name: Add NodeSource apt repository
ansible.builtin.apt_repository:
repo: "{{ openapply_app_nodesource_repo_map[ansible_os_family] }}"
filename: nodesource
state: present
- name: Install Node.js runtime
ansible.builtin.apt:
name: nodejs
state: present
update_cache: true
- name: Install pnpm globally
community.general.npm:
name: pnpm
global: true
state: present
- name: Install Firebase CLI globally
community.general.npm:
name: firebase-tools
global: true
state: present
when: openapply_app_install_firebase_cli | bool

View File

@ -0,0 +1,17 @@
---
- name: Ensure guest OS family is supported
ansible.builtin.assert:
that:
- ansible_os_family == "Debian"
fail_msg: "openapply_app currently supports Debian-family guests (Ubuntu 22.04/24.04)."
- name: Ensure required role variables are present
ansible.builtin.assert:
that:
- openapply_app_repo_url | length > 0
- openapply_app_repo_version | length > 0
- openapply_app_root | length > 0
- openapply_app_service_name | length > 0
- openapply_app_service_user | length > 0
- openapply_app_start_command | length > 0
fail_msg: "Required OpenApply role variables are missing."

View File

@ -0,0 +1,24 @@
---
- name: Gather service facts
ansible.builtin.service_facts:
- name: Assert OpenApply service is running
ansible.builtin.assert:
that:
- "ansible_facts.services[openapply_app_service_name + '.service'] is defined"
- "ansible_facts.services[openapply_app_service_name + '.service'].state == 'running'"
fail_msg: "OpenApply systemd service is not running."
- name: Wait for OpenApply port to become reachable
ansible.builtin.wait_for:
host: 127.0.0.1
port: "{{ openapply_app_service_port }}"
timeout: 120
- name: Validate HTTP response from OpenApply endpoint
ansible.builtin.uri:
url: "http://127.0.0.1:{{ openapply_app_service_port }}/"
method: GET
status_code: "{{ openapply_app_verify_status_codes }}"
register: openapply_http_probe
changed_when: false

View File

@ -0,0 +1,6 @@
{% for key, value in openapply_app_env.items() %}
{{ key }}={{ value }}
{% endfor %}
{% if openapply_app_firebase_token | length > 0 %}
FIREBASE_TOKEN={{ openapply_app_firebase_token }}
{% endif %}

View File

@ -0,0 +1,17 @@
[Unit]
Description=OpenApply web service
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
User={{ openapply_app_service_user }}
Group={{ openapply_app_service_group }}
WorkingDirectory={{ openapply_app_root }}
EnvironmentFile={{ openapply_app_root }}/.env
ExecStart={{ openapply_app_start_command }}
Restart=on-failure
RestartSec=3
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,3 @@
---
openapply_app_nodesource_repo_map:
Debian: "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_{{ openapply_app_node_major }}.x nodistro main"