Compare commits

..

2 Commits

Author SHA1 Message Date
boxswapper
364e59343f after migration 2026-11-13 13:01:22 -07:00
boxswapper
6d90e131b2 created new role 2026-11-13 12:26:24 -07:00
47 changed files with 425 additions and 136 deletions

View File

@@ -33,18 +33,35 @@ does.
## Layout
```
playbook.yml Ansible deployment (hosts: webservers)
inventory.ini Inventory (localhost + commented remote example)
trade-hub/
podman-compose.yml Local three-service stack (db + backend + frontend)
backend/ FastAPI app, tests, data fetch script, Dockerfile
frontend/ nginx config + static inventory UI
playbook.yml Example play using the role (hosts: webservers)
inventory.ini Inventory (localhost + commented remote example)
teardown.yml Removes this project's containers and data
roles/trade_hub/
README.md Role usage and variables
defaults/main.yml Configurable defaults
tasks/main.yml Stage source, build images, run containers
files/trade-hub/
podman-compose.yml Local three-service stack (db + backend + frontend)
backend/ FastAPI app, tests, data fetch script, Dockerfile
frontend/ nginx config + static inventory UI
```
The Ansible deployment is a self-contained role. To deploy it from an existing
playbook, copy `roles/trade_hub` into that repository's `roles/` directory and
add the role to the play:
```yaml
- role: trade_hub
tags: [trade_hub]
```
See `roles/trade_hub/README.md` for the variables. The play-level `become: true`
used by a typical server play makes podman run as root.
## Run locally with Podman Compose
```bash
cd trade-hub
cd roles/trade_hub/files/trade-hub
podman-compose up -d --build
# or: podman compose up -d --build
```
@@ -60,7 +77,8 @@ POSTGRES_PASSWORD='choose-something-strong' podman-compose up -d --build
## Deploy with Ansible
Deploys to every host in the `webservers` group (defaults to localhost):
The example play deploys the role to every host in the `webservers` group
(defaults to localhost):
```bash
POSTGRES_PASSWORD='choose-something-strong' ansible-playbook playbook.yml
@@ -73,16 +91,22 @@ To deploy to a remote server, edit `inventory.ini`:
server1 ansible_host=203.0.113.10 ansible_user=deploy
```
Then run the same command from this directory. The playbook:
Then run the same command from this directory. The role:
1. installs podman if missing,
2. removes the legacy `my-apache-server` container if present,
3. builds `sc-inventory-backend` / `sc-inventory-frontend` images (skipped when
they already exist; set `-e force_rebuild=true` to rebuild),
1. installs podman if missing (only when `trade_hub_install_podman: true`),
2. optionally removes the legacy `my-apache-server` container
(`trade_hub_remove_legacy_apache: true`),
3. stages the application source in `/opt/trade-hub` and builds the
`sc-inventory-backend` / `sc-inventory-frontend` images (skipped when the
source is unchanged and the images already exist; set
`-e trade_hub_force_rebuild=true` to force it),
4. creates the `sc-inventory` podman network and the `sc-inventory-data` volume,
5. runs PostgreSQL, the backend, and the frontend containers (only the frontend
publishes a port: 8080),
6. verifies `http://localhost:8080/healthz` returns 200.
6. verifies `http://127.0.0.1:8080/healthz` returns 200.
The old standalone playbook's variables (`force_rebuild`, `cookie_secure`, …)
became `trade_hub_*` variables; see `roles/trade_hub/README.md`.
## Reset for testing (remove everything)
@@ -165,7 +189,8 @@ The stack publishes plain HTTP on port 8080. Session cookies therefore travel
in cleartext, and `COOKIE_SECURE` defaults to `false` so local development works.
Before putting this on the open internet, terminate TLS in front of the
frontend (Caddy, nginx with certificates, or a load balancer) and set
`COOKIE_SECURE=true` (compose env var) or `-e cookie_secure=true` (Ansible).
`COOKIE_SECURE=true` (compose env var) or
`-e trade_hub_cookie_secure=true` (Ansible).
Also change `POSTGRES_PASSWORD` from its development default.
## Verify
@@ -195,7 +220,7 @@ curl -fsS -b cookies.txt http://localhost:8080/api/v1/me/inventory
## Backend development
```bash
cd trade-hub/backend
cd roles/trade_hub/files/trade-hub/backend
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt -r requirements-dev.txt
.venv/bin/python -m pytest tests/ -q # 83 tests, no database needed

View File

@@ -1,126 +1,23 @@
---
# Example play that exercises the trade_hub role against the local inventory.
# To use the role in an existing playbook, copy roles/trade_hub into that
# repository's roles/ directory and add:
#
# roles:
# - role: trade_hub
# tags: [trade_hub]
#
- name: Deploy Star Citizen Inventory (trade-hub)
hosts: webservers
vars:
backend_image: sc-inventory-backend
frontend_image: sc-inventory-frontend
postgres_image: docker.io/library/postgres:16-alpine
image_tag: latest
network_name: sc-inventory
frontend_port: "8080:80"
postgres_volume: sc-inventory-data
postgres_password: "{{ lookup('env', 'POSTGRES_PASSWORD') | default('sc_inventory_dev', true) }}"
database_url: "postgresql+psycopg://sc_inventory:{{ postgres_password }}@db:5432/sc_inventory"
# Set to true once the site is served over TLS.
cookie_secure: "false"
# Set to true to force image rebuilds even when the images already exist
force_rebuild: false
# The standalone play used to install podman; in a larger playbook the
# "packages" role normally provides it instead.
trade_hub_install_podman: true
trade_hub_postgres_password: "{{ lookup('env', 'POSTGRES_PASSWORD') | default('sc_inventory_dev', true) }}"
trade_hub_cookie_secure: "false"
trade_hub_force_rebuild: false
tasks:
- name: Check whether podman is installed
ansible.builtin.command: podman --version
register: podman_check
failed_when: false
changed_when: false
- name: Install podman if missing
become: true
ansible.builtin.package:
name: podman
state: present
when: podman_check.rc != 0
- name: Remove legacy Apache container if present
containers.podman.podman_container:
name: my-apache-server
state: absent
- name: Check if backend image exists
ansible.builtin.command: podman image exists {{ backend_image }}:{{ image_tag }}
register: backend_image_check
failed_when: false
changed_when: false
- name: Check if frontend image exists
ansible.builtin.command: podman image exists {{ frontend_image }}:{{ image_tag }}
register: frontend_image_check
failed_when: false
changed_when: false
- name: Build backend image
ansible.builtin.command: >
podman build -t {{ backend_image }}:{{ image_tag }}
{{ playbook_dir }}/trade-hub/backend
when: backend_image_check.rc != 0 or force_rebuild | bool
- name: Build frontend image
ansible.builtin.command: >
podman build -t {{ frontend_image }}:{{ image_tag }}
{{ playbook_dir }}/trade-hub/frontend
when: frontend_image_check.rc != 0 or force_rebuild | bool
- name: Create podman network
containers.podman.podman_network:
name: "{{ network_name }}"
state: present
- name: Create PostgreSQL data volume
containers.podman.podman_volume:
name: "{{ postgres_volume }}"
state: present
- name: Run PostgreSQL container (internal only)
containers.podman.podman_container:
name: sc-inventory-db
image: "{{ postgres_image }}"
state: started
network: "{{ network_name }}"
network_aliases:
- db
env:
POSTGRES_USER: sc_inventory
POSTGRES_PASSWORD: "{{ postgres_password }}"
POSTGRES_DB: sc_inventory
volumes:
- "{{ postgres_volume }}:/var/lib/postgresql/data:Z"
restart_policy: unless-stopped
- name: Run backend container (internal only)
containers.podman.podman_container:
name: sc-inventory-backend
image: "{{ backend_image }}:{{ image_tag }}"
state: started
# podman does not recreate a container when its image tag resolves to
# a new ID, so without this a rebuilt image would be ignored.
recreate: true
network: "{{ network_name }}"
# Network alias "backend" matches the compose service name, so the
# nginx upstream (http://backend:8000) resolves in both deployments.
network_aliases:
- backend
env:
DATABASE_URL: "{{ database_url }}"
DB_WAIT_SECONDS: "60"
COOKIE_SECURE: "{{ cookie_secure }}"
restart_policy: unless-stopped
- name: Run frontend container (published port)
containers.podman.podman_container:
name: sc-inventory-frontend
image: "{{ frontend_image }}:{{ image_tag }}"
state: started
recreate: true
network: "{{ network_name }}"
ports:
- "{{ frontend_port }}"
restart_policy: unless-stopped
- name: Verify the stack responds
ansible.builtin.uri:
url: http://localhost:8080/healthz
status_code: 200
register: health_result
until: health_result.status == 200
retries: 8
delay: 10
roles:
- role: trade_hub
tags: [trade_hub]

85
roles/trade_hub/README.md Normal file
View File

@@ -0,0 +1,85 @@
# trade_hub
Deploys the Free People of the Verse trade hub: a FastAPI backend, an nginx
frontend and PostgreSQL, all run by podman on the target host. The role stages
its own copy of the application source from `files/trade-hub/` and builds the
backend and frontend images locally, so it does not need a registry.
## Usage
Add the role to an existing play. It is safe to run after a `packages` role that
already installs podman:
```yaml
- hosts: internal-apps
become: true
vars_files:
- ./vars.yml
roles:
- role: packages
tags: [packages]
# ...
- role: trade_hub
tags: [trade_hub]
```
The play-level `become: true` makes podman run as root. If you run the role
without become, podman runs rootless and the containers belong to the connecting
user.
The playbook's `Pull images` task is unrelated to this role: the role builds its
own backend/frontend images from the staged source. The PostgreSQL image is
pulled by the role if it is not already present.
## Required variables
Set a real database password in `vars.yml` (or the environment of the control
node):
```yaml
trade_hub_postgres_password: "choose-something-strong"
```
If it is omitted, the role falls back to the play variable `POSTGRES_PASSWORD`,
then to `POSTGRES_PASSWORD` from the control node environment, then to the
development default `sc_inventory_dev`. If the password contains characters
that are significant in a URL (`@`, `:`, `/`, …), set `trade_hub_database_url`
explicitly as well.
## Common variables
| Variable | Default | Purpose |
| --- | --- | --- |
| `trade_hub_app_dir` | `/opt/trade-hub` | Where the build context is staged |
| `trade_hub_install_podman` | `false` | Install podman if it is missing |
| `trade_hub_build_images` | `true` | Build backend/frontend images on the target |
| `trade_hub_force_rebuild` | `false` | Rebuild images and restart containers |
| `trade_hub_image_tag` | `latest` | Tag applied to built images |
| `trade_hub_backend_image` | `localhost/sc-inventory-backend` | Backend image name |
| `trade_hub_frontend_image` | `localhost/sc-inventory-frontend` | Frontend image name |
| `trade_hub_postgres_image` | `docker.io/library/postgres:16-alpine` | Database image |
| `trade_hub_postgres_user` | `sc_inventory` | Database user |
| `trade_hub_postgres_db` | `sc_inventory` | Database name |
| `trade_hub_postgres_password` | play var/env `POSTGRES_PASSWORD` or `sc_inventory_dev` | Database password |
| `trade_hub_network` | `sc-inventory` | Podman network name |
| `trade_hub_volume` | `sc-inventory-data` | PostgreSQL data volume |
| `trade_hub_frontend_bind` | `0.0.0.0` | Address the frontend publishes on |
| `trade_hub_frontend_port` | `8080` | Host port for the frontend |
| `trade_hub_cookie_secure` | `false` | Mark session cookies `Secure` (enable behind TLS) |
| `trade_hub_healthcheck_url` | `http://127.0.0.1:<port>/healthz` | Post-deploy check |
| `trade_hub_remove_legacy_apache` | `false` | Remove a container named `my-apache-server` |
| `trade_hub_enable_restart_service` | `false` | Enable `podman-restart.service` |
The container names also default to `sc-inventory-db`, `sc-inventory-backend`
and `sc-inventory-frontend`, matching the previous standalone playbook so an
existing installation is upgraded in place.
## Example deployment
```bash
ansible-playbook site.yml --tags trade_hub -e trade_hub_postgres_password='...'
```
The role verifies `http://127.0.0.1:8080/healthz` after starting the containers.
Only the frontend publishes a port; the database and backend stay on the
internal podman network.

View File

@@ -0,0 +1,51 @@
---
# Where the application build context is staged on the target host.
trade_hub_app_dir: /opt/trade-hub
# Install podman as part of this role. Leave false when another role (for
# example "packages") already provides podman.
trade_hub_install_podman: false
# Images. These are built locally by default; point them at a registry if you
# prefer to pull prebuilt images.
trade_hub_image_tag: latest
trade_hub_backend_image: localhost/sc-inventory-backend
trade_hub_frontend_image: localhost/sc-inventory-frontend
trade_hub_postgres_image: docker.io/library/postgres:16-alpine
trade_hub_build_images: true
trade_hub_force_rebuild: false
# Container names. Kept compatible with the previous standalone playbook so an
# existing installation is upgraded in place.
trade_hub_db_container: sc-inventory-db
trade_hub_backend_container: sc-inventory-backend
trade_hub_frontend_container: sc-inventory-frontend
# Database.
trade_hub_postgres_user: sc_inventory
trade_hub_postgres_db: sc_inventory
trade_hub_postgres_password: "{{ POSTGRES_PASSWORD | default(lookup('env', 'POSTGRES_PASSWORD'), true) | default('sc_inventory_dev', true) }}"
trade_hub_database_url: "postgresql+psycopg://{{ trade_hub_postgres_user }}:{{ trade_hub_postgres_password }}@db:5432/{{ trade_hub_postgres_db }}"
trade_hub_db_wait_seconds: 60
# Networking.
trade_hub_network: sc-inventory
trade_hub_volume: sc-inventory-data
trade_hub_frontend_bind: 0.0.0.0
trade_hub_frontend_port: 8080
# Set to true (or "true") once the site is served over TLS.
trade_hub_cookie_secure: "false"
# Post-deploy health check.
trade_hub_healthcheck_url: "http://127.0.0.1:{{ trade_hub_frontend_port }}/healthz"
trade_hub_healthcheck_retries: 12
trade_hub_healthcheck_delay: 5
# The original standalone playbook removed an ad-hoc Apache container that
# used the same host. Disabled by default so the role never touches unrelated
# containers.
trade_hub_remove_legacy_apache: false
trade_hub_legacy_apache_container: my-apache-server
# Enable podman-restart.service so the containers come back after a reboot.
trade_hub_enable_restart_service: false

View File

@@ -0,0 +1,16 @@
---
galaxy_info:
role_name: trade_hub
author: boxswapper
description: Deploy the Free People of the Verse trade hub (FastAPI, nginx, PostgreSQL) with Podman.
license: MIT
min_ansible_version: "2.15"
platforms:
- name: GenericLinux
versions:
- all
galaxy_tags:
- podman
- fastapi
- postgresql
dependencies: []

View File

@@ -0,0 +1,215 @@
---
- name: Check whether podman is installed
ansible.builtin.command: podman --version
register: trade_hub_podman_check
changed_when: false
failed_when: false
check_mode: false
- name: Install podman when requested
ansible.builtin.package:
name: podman
state: present
become: true
when:
- trade_hub_podman_check.rc != 0
- trade_hub_install_podman | bool
- name: Re-check podman after installation
ansible.builtin.command: podman --version
register: trade_hub_podman_recheck
changed_when: false
failed_when: false
check_mode: false
when: trade_hub_podman_check.rc != 0
- name: Require podman to be available
ansible.builtin.assert:
that:
- trade_hub_podman_check.rc == 0 or (trade_hub_podman_recheck.rc | default(1)) == 0
fail_msg: >-
podman is not installed on {{ inventory_hostname }}. Install it first
(for example with the "packages" role) or run this role with
-e trade_hub_install_podman=true.
- name: Remove the legacy Apache container when requested
containers.podman.podman_container:
name: "{{ trade_hub_legacy_apache_container }}"
state: absent
when: trade_hub_remove_legacy_apache | bool
- name: Create the application directory
ansible.builtin.file:
path: "{{ trade_hub_app_dir }}"
state: directory
mode: "0755"
- name: Stage the application build context
ansible.builtin.copy:
src: "{{ role_path }}/files/trade-hub/{{ item.src }}"
dest: "{{ trade_hub_app_dir }}/{{ item.dest }}"
mode: preserve
loop:
# Only the files needed by the two Dockerfiles are staged. Copying the
# whole tree would also ship local development artefacts such as .venv.
- { src: "backend/Dockerfile", dest: "backend/Dockerfile" }
- { src: "backend/requirements.txt", dest: "backend/requirements.txt" }
- { src: "backend/main.py", dest: "backend/main.py" }
- { src: "backend/app/", dest: "backend/app/" }
- { src: "backend/scripts/", dest: "backend/scripts/" }
- { src: "frontend/Dockerfile", dest: "frontend/Dockerfile" }
- { src: "frontend/nginx.conf", dest: "frontend/nginx.conf" }
- { src: "frontend/src/", dest: "frontend/src/" }
register: trade_hub_source
- name: Check whether the backend image exists
ansible.builtin.command:
argv:
- podman
- image
- exists
- "{{ trade_hub_backend_image }}:{{ trade_hub_image_tag }}"
register: trade_hub_backend_image_check
changed_when: false
failed_when: false
check_mode: false
- name: Check whether the frontend image exists
ansible.builtin.command:
argv:
- podman
- image
- exists
- "{{ trade_hub_frontend_image }}:{{ trade_hub_image_tag }}"
register: trade_hub_frontend_image_check
changed_when: false
failed_when: false
check_mode: false
- name: Build the backend image
ansible.builtin.command:
argv:
- podman
- build
- --tag
- "{{ trade_hub_backend_image }}:{{ trade_hub_image_tag }}"
- "{{ trade_hub_app_dir }}/backend"
when:
- trade_hub_build_images | bool
- trade_hub_force_rebuild | bool or trade_hub_source.changed or trade_hub_backend_image_check.rc != 0
register: trade_hub_backend_build
- name: Build the frontend image
ansible.builtin.command:
argv:
- podman
- build
- --tag
- "{{ trade_hub_frontend_image }}:{{ trade_hub_image_tag }}"
- "{{ trade_hub_app_dir }}/frontend"
when:
- trade_hub_build_images | bool
- trade_hub_force_rebuild | bool or trade_hub_source.changed or trade_hub_frontend_image_check.rc != 0
register: trade_hub_frontend_build
- name: Require prebuilt images when image builds are disabled
ansible.builtin.assert:
that:
- trade_hub_backend_image_check.rc == 0
- trade_hub_frontend_image_check.rc == 0
fail_msg: >-
trade_hub_build_images is false but {{ trade_hub_backend_image }}:{{
trade_hub_image_tag }} and/or {{ trade_hub_frontend_image }}:{{
trade_hub_image_tag }} are missing on {{ inventory_hostname }}.
when: not trade_hub_build_images | bool
- name: Record whether the application containers need to be recreated
ansible.builtin.set_fact:
trade_hub_recreate_containers: >-
{{ (trade_hub_force_rebuild | bool)
or ((trade_hub_build_images | bool) and (trade_hub_source.changed | default(false)))
or (trade_hub_backend_build.changed | default(false))
or (trade_hub_frontend_build.changed | default(false)) }}
- name: Ensure the PostgreSQL image is present
containers.podman.podman_image:
name: "{{ trade_hub_postgres_image }}"
state: present
when: not ansible_check_mode
- name: Create the podman network
containers.podman.podman_network:
name: "{{ trade_hub_network }}"
state: present
- name: Create the PostgreSQL data volume
containers.podman.podman_volume:
name: "{{ trade_hub_volume }}"
state: present
- name: Run the PostgreSQL container
containers.podman.podman_container:
name: "{{ trade_hub_db_container }}"
image: "{{ trade_hub_postgres_image }}"
state: started
recreate: "{{ trade_hub_force_rebuild | bool }}"
network: "{{ trade_hub_network }}"
network_aliases:
- db
env:
POSTGRES_USER: "{{ trade_hub_postgres_user }}"
POSTGRES_PASSWORD: "{{ trade_hub_postgres_password }}"
POSTGRES_DB: "{{ trade_hub_postgres_db }}"
volumes:
- "{{ trade_hub_volume }}:/var/lib/postgresql/data:Z"
restart_policy: unless-stopped
when: not ansible_check_mode
- name: Run the backend container
containers.podman.podman_container:
name: "{{ trade_hub_backend_container }}"
image: "{{ trade_hub_backend_image }}:{{ trade_hub_image_tag }}"
state: started
# podman does not recreate a container when its image tag resolves to a new
# ID, so recreate whenever the image was rebuilt or the app changed.
recreate: "{{ trade_hub_recreate_containers | bool }}"
network: "{{ trade_hub_network }}"
# The nginx upstream is http://backend:8000, so this alias must match.
network_aliases:
- backend
env:
DATABASE_URL: "{{ trade_hub_database_url }}"
DB_WAIT_SECONDS: "{{ trade_hub_db_wait_seconds }}"
COOKIE_SECURE: "{{ trade_hub_cookie_secure | bool | lower }}"
restart_policy: unless-stopped
when: not ansible_check_mode
- name: Run the frontend container
containers.podman.podman_container:
name: "{{ trade_hub_frontend_container }}"
image: "{{ trade_hub_frontend_image }}:{{ trade_hub_image_tag }}"
state: started
recreate: "{{ trade_hub_recreate_containers | bool }}"
network: "{{ trade_hub_network }}"
ports:
- "{{ trade_hub_frontend_bind }}:{{ trade_hub_frontend_port }}:80"
restart_policy: unless-stopped
when: not ansible_check_mode
- name: Enable podman-restart so containers survive a reboot
ansible.builtin.systemd:
name: podman-restart.service
enabled: true
state: started
when: trade_hub_enable_restart_service | bool
- name: Wait for the application to answer health checks
ansible.builtin.uri:
url: "{{ trade_hub_healthcheck_url }}"
status_code: 200
register: trade_hub_health
until: trade_hub_health.status == 200
retries: "{{ trade_hub_healthcheck_retries }}"
delay: "{{ trade_hub_healthcheck_delay }}"
changed_when: false
when: not ansible_check_mode