feat(app_setup): improve deployment reliability and add mirrormatch support
All checks were successful
CI / skip-ci-check (pull_request) Successful in 1m22s
CI / lint-and-test (pull_request) Successful in 1m27s
CI / ansible-validation (pull_request) Successful in 2m53s
CI / secret-scanning (pull_request) Successful in 1m24s
CI / dependency-scan (pull_request) Successful in 1m28s
CI / sast-scan (pull_request) Successful in 2m32s
CI / license-check (pull_request) Successful in 1m28s
CI / vault-check (pull_request) Successful in 2m30s
CI / playbook-test (pull_request) Successful in 2m32s
CI / container-scan (pull_request) Successful in 1m53s
CI / sonar-analysis (pull_request) Successful in 2m40s
CI / workflow-summary (pull_request) Successful in 1m22s

- Fix deploy script to handle non-git directories by cloning to temp
  location and moving contents, preserving .env files during clone
- Remove comment lines from env.j2 template to prevent xargs errors
- Add initial deploy task to app_setup role to ensure app is deployed
  before service starts
- Fix migrate command precedence to check env-specific overrides first
- Add sudo to systemctl restart commands in deploy script
- Update documentation with project-specific configuration notes

These changes improve deployment reliability for all app projects while
adding support for mirrormatch-specific requirements (db:push, seeding).
All changes are backward-compatible with existing projects (pote, punimTag).
This commit is contained in:
ilia 2026-01-04 16:50:54 -05:00
parent c3e6caf9e8
commit c2e797a027
13 changed files with 374 additions and 79 deletions

View File

@ -25,6 +25,7 @@ Under `app_projects`, define projects like:
- `projectA.envs.dev|qa|prod.ip/gateway/branch`
- `projectA.guest_defaults` (cores/memory/rootfs sizing)
- `projectA.deploy.*` (install/build/migrate/start commands)
- Optional: per-env `backend_seed_cmd` (e.g., `npm run db:seed` for dev/qa)
Adding **projectB** is just adding another top-level `app_projects.projectB` entry.
@ -77,6 +78,34 @@ Only OS/app configuration:
ansible-playbook -i inventories/production playbooks/app/configure_app.yml -e app_project=projectA
```
### Limiting to a single env (dev/qa/prod)
- Pass `-e app_env=dev` (or qa/prod) to provision/configure only that environment for the selected project.
- Example (provision dev only):
```bash
ansible-playbook -i inventories/production playbooks/app/provision_vms.yml -e app_project=mirrormatch -e app_env=dev
```
Configure/dev only:
```bash
ansible-playbook -i inventories/production playbooks/app/configure_app.yml -e app_project=mirrormatch -e app_env=dev
```
### Example: mirrormatch project
- Config lives in `inventories/production/group_vars/all/main.yml` under `app_projects.mirrormatch` (dev/qa/prod guests, repo URL, migrate/start commands, env vars).
- Secrets live in the vault: `vault_mirrormatch_database_url_*` (and optional `vault_mirrormatch_shadow_database_url_*`), plus an optional repo deploy key `vault_mirrormatch_git_ssh_key`.
- Run end-to-end:
```bash
make app PROJECT=mirrormatch
```
Run provisioning only / configure only:
```bash
make app-provision PROJECT=mirrormatch
make app-configure PROJECT=mirrormatch
```
## Optional: SSH aliases on your workstation
To write `~/.ssh/config` entries (disabled by default):
@ -87,4 +116,37 @@ ansible-playbook -i inventories/production playbooks/app/ssh_client_config.yml -
This creates aliases like `projectA-dev`, `projectA-qa`, `projectA-prod`.
## Project-Specific Configuration
### Environment-Specific Commands
Projects can override deploy commands per environment:
```yaml
envs:
dev:
backend_migrate_cmd: "npm run db:push" # Override default migrate command
backend_seed_cmd: "npm run db:seed" # Optional: seed database
```
**Precedence order:**
1. `env_def.backend_migrate_cmd` (per-environment override)
2. `project_def.deploy.backend_migrate_cmd` (project default)
3. Global default (`npm run migrate`)
### Environment File Naming
The systemd service uses `EnvironmentFile=/srv/app/.env.<env>` (e.g., `.env.dev`). Systemd loads these variables into the service environment.
**Note:** Next.js has its own env file loading that looks for `.env`, `.env.local`, `.env.production`, etc. If your Next.js app isn't reading env vars, consider:
- Using `.env.local` for dev (Next.js loads this automatically)
- Or ensure your app reads from `process.env` (systemd-injected vars)
### Project Types
- **Standard Node.js apps** (mirrormatch, punimTagBE/FE): Use `app_setup` role
- **Python apps** (pote): Use `pote` role (completely separate deployment path)
Changes to `app_setup` role affect all Node.js projects but are backward-compatible.

View File

@ -296,3 +296,92 @@ app_projects:
env_vars:
APP_ENV: "prod"
SECRET_PLACEHOLDER: "change-me"
mirrormatch:
description: "Mirrormatch Prisma/Node backend (dev/qa/prod)."
repo_url: "gitea@10.0.30.169:ilia/mirror_match.git"
repo_dest: "/srv/app"
backend_dir: "/srv/app"
components:
backend: true
frontend: false
# Next.js app listens on 3000 by default
backend_port: 3000
guest_defaults:
guest_type: "{{ proxmox_guest_type }}"
cores: 2
memory_mb: 2048
swap_mb: 512
rootfs_size_gb: 16
deploy:
backend_install_cmd: "npm ci"
backend_build_cmd: "npm run build"
backend_migrate_cmd: "npm run db:migrate"
backend_start_cmd: "npm run start"
envs:
dev:
name: "mirrormatch-dev"
vmid: 9401
ip: "10.0.10.141/24"
gateway: "10.0.10.1"
branch: "dev"
backend_migrate_cmd: "npm run db:push"
backend_seed_cmd: "npm run db:seed"
env_vars:
APP_ENV: "dev"
NODE_ENV: "production"
PORT: "3000"
DATABASE_URL: "{{ vault_mirrormatch_database_url_dev }}"
NEXTAUTH_URL: "https://mirrormatchdev.levkin.ca"
NEXTAUTH_SECRET: "{{ vault_mirrormatch_nextauth_secret_dev }}"
AUTH_TRUST_HOST: "true"
# Dev/QA often use Ethereal/console email; leave SMTP unset or fill if needed
# SMTP_HOST: ""
# SMTP_PORT: ""
# SMTP_USER: ""
# SMTP_PASSWORD: ""
# SMTP_FROM: ""
# Optional: provide if your Prisma workflow needs a shadow DB
# SHADOW_DATABASE_URL: "{{ vault_mirrormatch_shadow_database_url_dev }}"
qa:
name: "mirrormatch-qa"
vmid: 9402
ip: "10.0.10.144/24"
gateway: "10.0.10.1"
branch: "qa"
backend_migrate_cmd: "npm run db:push"
backend_seed_cmd: "npm run db:seed"
env_vars:
APP_ENV: "qa"
NODE_ENV: "production"
PORT: "3000"
DATABASE_URL: "{{ vault_mirrormatch_database_url_qa }}"
NEXTAUTH_URL: "https://mirrormatchqa.levkin.ca"
NEXTAUTH_SECRET: "{{ vault_mirrormatch_nextauth_secret_qa }}"
AUTH_TRUST_HOST: "true"
# SMTP_HOST: ""
# SMTP_PORT: ""
# SMTP_USER: ""
# SMTP_PASSWORD: ""
# SMTP_FROM: ""
# SHADOW_DATABASE_URL: "{{ vault_mirrormatch_shadow_database_url_qa }}"
prod:
name: "mirrormatch-prod"
vmid: 9403
ip: "10.0.10.145/24"
gateway: "10.0.10.1"
branch: "main"
backend_migrate_cmd: "npx prisma migrate deploy"
env_vars:
APP_ENV: "prod"
NODE_ENV: "production"
PORT: "3000"
DATABASE_URL: "{{ vault_mirrormatch_database_url_prod }}"
NEXTAUTH_URL: "https://mirrormatch.example.com"
NEXTAUTH_SECRET: "{{ vault_mirrormatch_nextauth_secret_prod }}"
AUTH_TRUST_HOST: "true"
SMTP_HOST: "{{ vault_mirrormatch_smtp_host | default('') }}"
SMTP_PORT: "{{ vault_mirrormatch_smtp_port | default('587') }}"
SMTP_USER: "{{ vault_mirrormatch_smtp_user | default('') }}"
SMTP_PASSWORD: "{{ vault_mirrormatch_smtp_password | default('') }}"
SMTP_FROM: "{{ vault_mirrormatch_smtp_from | default('') }}"

View File

@ -39,4 +39,35 @@ vault_pote_db_password_prod: "CHANGE_ME"
# SMTP password for reports
vault_pote_smtp_password: "CHANGE_ME"
# -----------------------------------------------------------------------------
# Mirrormatch (Prisma/Node backend) secrets
# -----------------------------------------------------------------------------
# Optional deploy key for private repo access
vault_mirrormatch_git_ssh_key: |
-----BEGIN OPENSSH PRIVATE KEY-----
CHANGE_ME
-----END OPENSSH PRIVATE KEY-----
# Per-environment database URLs (use external Postgres VM/cluster)
vault_mirrormatch_database_url_dev: "postgresql://mm_dev_user:CHANGE_ME@10.0.10.181:5432/mirrormatch_dev"
vault_mirrormatch_database_url_qa: "postgresql://mm_qa_user:CHANGE_ME@10.0.10.181:5432/mirrormatch_qa"
vault_mirrormatch_database_url_prod: "postgresql://mm_prod_user:CHANGE_ME@10.0.10.181:5432/mirrormatch_prod"
# Optional shadow DB URLs if your Prisma workflow needs them
vault_mirrormatch_shadow_database_url_dev: "postgresql://mm_dev_shadow:CHANGE_ME@10.0.10.181:5432/mirrormatch_dev_shadow"
vault_mirrormatch_shadow_database_url_qa: "postgresql://mm_qa_shadow:CHANGE_ME@10.0.10.181:5432/mirrormatch_qa_shadow"
vault_mirrormatch_shadow_database_url_prod: "postgresql://mm_prod_shadow:CHANGE_ME@10.0.10.181:5432/mirrormatch_prod_shadow"
# NEXTAUTH secrets per env
vault_mirrormatch_nextauth_secret_dev: "CHANGE_ME"
vault_mirrormatch_nextauth_secret_qa: "CHANGE_ME"
vault_mirrormatch_nextauth_secret_prod: "CHANGE_ME"
# SMTP (prod)
vault_mirrormatch_smtp_host: "smtp.example.com"
vault_mirrormatch_smtp_port: "587"
vault_mirrormatch_smtp_user: "smtp-user"
vault_mirrormatch_smtp_password: "CHANGE_ME"
vault_mirrormatch_smtp_from: "MirrorMatch <noreply@mirrormatch.com>"

View File

@ -1,47 +1,100 @@
$ANSIBLE_VAULT;1.1;AES256
36643038376636383030343730626264613839396462366365633837636130623639393361656634
3238353261633635353662653036393835313963373562390a646535376366656163383632313835
39646666653362336661633736333365343962346432653131613134353361366263373162386631
3134613438626132320a313765343338643535343837306339616564336564303166626164356530
63663363656535303137663431613861343662303664313332626166373463393931323937613230
66333665316331323637663437653339353737653336633864393033336630336438646162643662
31656164363933333036376263303034646366393134636630663631353235373831303264363762
66643865616130306537383836646237613730643133656333666632326538613764383530363363
61386161646637316166303633643665383365346534323939383034613430386362303038313761
36303364396436373466653332303562653038373962616539356633373065643130303036363161
65353163326136383066393332376236386333653532326337613163346334616234643562643265
62316134386365343733636661336130623364386634383965386135616633323132643365613231
34636435333031376136396336316337666161383562343834383865316436633333333065323138
37343865363731303137666330306131373734623637343531623562353332353437646631343363
30393565376435303430396535643165616534313334326462363130626639343038643835336335
33613630336534666163356631353438373462306566376134323536373832643264633365653465
62386363326436623330653430383262653732376235626432656362306363303663623834653664
31373762306539376431353137393664396165396261613364653339373765393863633833396131
36666235666234633430373338323331313531643736656137303937653865303431643164373161
39633238383265396366386230303536613461633431333565353433643935613231333232333063
36643435376165656262623863373039393837643564366531666462376162653630626634663037
39373439336239646131306133663566343734656339346462356662373561306264333364383966
38343463616666613037636335333137633737666166633364343736646232396566373866633531
34303734376137386363373039656565323364333539626630323465666636396465323861333365
35376161663630356132373638333937376164316361303531303637396334306133373237656265
36356532623130323565396531306136363339363437376364343138653139653335343765316365
38313035366137393365316139326236326330386365343665376335313339666231333632333133
32353865626531373462346261653832386234396531653136323162653865303861396233376261
34616232363965313635373833333737336166643734373633313865323066393930666562316136
36373763356365646361656436383463393237623461383531343134373336663763663464336361
38396532383932643065303731663565353366373033353237383538636365323064396531386134
61643964613930373439383032373364316437303239393434376465393639373634663738623461
37386366616333626434363761326361373533306635316164316363393264303633353939613239
37353266303637323139653630663236663633313061306633316139666539376632306630313362
34633834326433646230303634313266303530633236353262633066396462646365623935343161
34393166643666366164313438383939386434366665613330653739383139613732396633383261
33633664303131383163356362316639353064373861343132623565636631333135663034373461
61303031616634333235303066633939643337393862653031323936363932633438303035323238
66323066353737316166383533636661336637303265343937633064626164623462656134333732
33316536336430636636646561626232666633656266326339623732363531326131643764313838
62356537326166346666313930383639386466633432626235373738633833393164646238366465
62373938363739373036666238666433303061633732663565666433333631326432626461353037
39636263636632313431353364386566383134653139393762623562643561616166633035353038
39326462356332616563303462636536636132633933336532383938373030666333363264346632
64643063373830353130613662323131353964313038323735626464313363326364653732323732
3663393964633138376665323435366463623463613237366465
38316537376634623462313731323238666165383731656632373665653534623163386333303865
3865383030316132663831303932376437346335323233630a643331663539383163306666393764
38313265656561343839616565343663353037663237663032366632373831363336306632626266
3361643865333533340a356233663034343932323831323236356161396237346532323838373135
33393239313730363336613338373039663735323431323562613363343863326234633833663631
66343462623231663932633537373361313764393630356666393662653135356139663935613038
65383261363065633235343031346535373564373931373063386265343335623265653739613830
32656233393330633362623932316431383761306332393466313936396533333839313831663331
34353864356336303331663233653666363966376162303731626134313235306238323363303439
32333039653235326632303637303065386161616138356463623561366637376366326262303166
38323763393934666539373063323265333961666164613437316164633565393035626538353365
33386562336665383863636639643232623161643933313664396534383362303838663362653736
64393334616165336638306235363734653431646431616139373336656333623963386538646230
39663230363063386231343730663162313463666135323265613261626637626332353534396535
31623664363766646332396336396133613662643232366433323330373962633839613635333763
63306230623438346639323863353137363330316630316130326134323731326635643736373736
62336362656265633233623165376436373231656666303832373966353732313031623865316663
63356163636238346230623732326232646434623532633439646536656362393162613535613565
66616539316362376561386263373464623030636661663435383839643565393632616232663035
34653735383964653930633664346330386566343830336238306562343164366131643138643339
35313366356637643262636238366263353535306434633732623335643266396335666636666663
37333232393765306433326164663538663839623034373535653737633366303665633831303334
32303061363863386139613464326466336136396534663538643163343439343763383534306636
62353733613330376163386331626463656462336237656339356132643135363537343638303261
33366332653439313137613665386136666536356537346665333935366336623734393738346434
63326265346362636564366265373134336662626332653464646139656635313961656230336537
63666638326337643033363964643339666130386139363138656165666333356465643337396165
30336330633632353231613938646165383966613863366330646162646266346139343434393865
66346365663230626531643963383462636465363965393762336233366538393133313138616335
32353834313762363265643031343237633732393166343139363163326439666162396332353038
31306530626666343361313736313636613335376163383237303063393333386663333333336137
37346166316231623638386635613230663063653037643930333961316434643361633035633734
65643937636361653433383262643265373165613437336236633631323635613034663834646665
30373730373438613132633932333565376665333565383932356334653738646166393934626362
30666666303832613633316230623038343165396338343535663931383639623430643238656261
39623037333063306266323335303736346236636137633863353866343136346335353865303961
31346331333066376330306361396262333762393838303165383134303435353630366130303536
34386532356239326166386665623435646432636561363564656161646563306234333138333839
38316337656631313763393135396464643338386636336234346663653538353863643636323032
35326133623064363838386662653138613438386564316635373838366262656364666633636539
61306563666138656161336466323537626161313366616662623362643036636132663634313137
39653437306662646162613763343736636530356465346132646238633166373838353836326461
36326666323636353239303262623436643932353164323630326635653635653233363265316264
30653763643431626539356161376534396437636463303363663134373961616561363561333333
34306537326666383664336464656464623731656566653132613565336536323438666333366466
64613738653730333633383062653837366266316536653139643362373039383831363666333934
34383833336266356436666636323239336432386133303466636138643934356266326533643161
36393664313963393930383533623565383332613933396639613037323266663439313138326261
30353861303661303836343165353362663632306430626337356562343637653164396237333566
37656230363530323836373363646334356262646633313932383161303264613238373936353036
61376264633930356465626266623930333039383032316163633037323035346130343934616261
31666166393462366561303833353135326566356637376466613934376233303162323033623031
63656131333439353537623662363530383866326432306361316465383137633536666364623662
37353561633839623530333663643130326131333330626661396636343234666139336539653162
62383636663137626637303535333862366434626161353239393232313537343865646564626331
39366665363030643764663963316163343033326434373265343664393439316333346434363563
61346164396561343865626362616433306230333130653166656230353364316536626432373333
35383133363530666263316431396462383133363965336637386632363263656261353963313161
36383632326264373436383638383064346334336238656239393833653531656461356136303434
37663434663732306631656334306361663562303863386135623066633963373034373139666332
35393433646333363839666434663535363661616330386234366132303161383063663836626561
35393064343735303032313266643338623834383838633834636536363539656466663864613366
66636363623330326436363936313938333638323939323035616232366563316364343834376630
66656434336661643861613737616138396330383832386230383331646462323363373363393733
63363237636137373566363438663966396432613964336164326138623737393636396234646232
64343361363365356135666235623833396131626663303839653535663732313831633163643638
35396262373837343238343838663635353838373338663732626330613237623332336436643136
38653833383430393837383566643765653834306636356466326364303334653034626262356630
34333338333336373433356235386337346666343830303164363235303265313134323339653339
63316238346132653663653165313635336638646362356337643766366564383531633565303431
66616433663630343439336661346266336139613537653438653432326666326137306364376137
66333939643262633532363966623439373434393862353237613135646663623236646331643537
31353566653464313433636635393330646166613232633734346639326534373163383064353732
32373861303064346266643338316465653031646633633936373738663837383162643534623131
31633662356534343636313834386139656439663733333762323962323939623032396239356437
37633739613433613365313337383835623936623530363831383535663337343264356532616434
39393634396664636166346631313764343733666534613935393637363233373331303837656463
37363266363634353136316532333462396266373733333633356239653334363835326261323661
66323032346364356230613831643236316530356132343863393361343462373433383265336333
30343730316366366234333263343965633466333439653739663333643939303631353664316435
36396139623562656632666165666662626263643436396431326135633932393965656531633761
39303634643936366438336534613532303134343164326661626363656562383564623264636132
39656636303636393761653035303832386430646162343830343834316534636263373763643765
61366335643531666232303231656336643833396238336639333437363564636566636632303364
62623738336237393638363436396662656565653839643164356565313563663561666237383036
33626464663465643230376164653062663063636630613064643632643235643662653566333333
62353763643830363638323731303537633837393235656661333263323536363330356362643333
34346666656432626365383639326538643862346265316263326531623631383962383734316330
39333430613761663337306331623461643635653431343336663163343766373464366538313335
61643538643231333636643836663663313534356662386532633331346664653262353839643066
36393366653131316636646336313362656662666163333635633132323438353435373430643839
37623936393962333065663536306238653466363634386632366637363265303734356535333735
64623330303965393533326563643063303762646664666464643239386435343065326234306632
35346338373866303838613933653230373737396134653533376265356432333933356237636338
66656536393530316435323863373962636465333331653364626162326562393565313538633264
34613633393862333731336563636136666166613037613833333063303162373339663539646631
36303962356562306239616634376339356135666663303836353061663039343836356262373932
65346466373532633365383835323062313531623130396130376531626333653862393462643631
366330333666336262373364663864336633

View File

@ -32,7 +32,7 @@ listmonk ansible_host=10.0.10.149 ansible_user=root
nextcloud ansible_host=10.0.10.25 ansible_user=root
actual ansible_host=10.0.10.158 ansible_user=root
vikanjans ansible_host=10.0.10.159 ansible_user=root
n8n ansible_host=10.0.10.158 ansible_user=root
n8n ansible_host=10.0.10.154 ansible_user=root
giteaVM ansible_host=10.0.30.169 ansible_user=root
portainerVM ansible_host=10.0.30.69 ansible_user=ladmin
homepageVM ansible_host=10.0.30.12 ansible_user=homepage

View File

@ -20,6 +20,12 @@
if (app_project is not defined or app_project | length == 0)
else [app_project]
}}
selected_envs: >-
{{
[app_env]
if (app_env is defined and app_env | length > 0)
else ['dev', 'qa', 'prod']
}}
app_bootstrap_user_default: root
# If true, configure plays will use vault_lxc_root_password for initial SSH bootstrap.
bootstrap_with_root_password_default: false
@ -47,7 +53,7 @@
}}
app_project: "{{ item.0 }}"
app_env: "{{ item.1 }}"
loop: "{{ selected_projects | product(['dev', 'qa', 'prod']) | list }}"
loop: "{{ selected_projects | product(selected_envs) | list }}"
when:
- app_projects[item.0] is defined
- app_projects[item.0].envs[item.1] is defined
@ -116,16 +122,20 @@
# app_env is already set per-host via add_host (dev/qa/prod)
app_owner: "{{ project_def.os_user | default(appuser_name) }}"
app_group: "{{ project_def.os_user | default(appuser_name) }}"
app_backend_dir: "{{ project_def.backend_dir | default(project_def.repo_dest | default(app_repo_dest)) }}"
app_frontend_dir: "{{ project_def.frontend_dir | default(project_def.repo_dest | default(app_repo_dest) + '/frontend') }}"
# Use different variable names to avoid self-referential recursion
app_backend_port: "{{ project_def.backend_port | default(3001) }}"
app_backend_port: "{{ project_def.backend_port | default(3000) }}"
app_frontend_port: "{{ project_def.frontend_port | default(3000) }}"
app_enable_backend: "{{ project_def.components.backend | default(true) }}"
app_enable_frontend: "{{ project_def.components.frontend | default(true) }}"
app_backend_install_cmd: "{{ project_def.deploy.backend_install_cmd | default('npm ci') }}"
app_backend_migrate_cmd: "{{ project_def.deploy.backend_migrate_cmd | default('npm run migrate') }}"
app_backend_start_cmd: "{{ project_def.deploy.backend_start_cmd | default('npm start') }}"
app_backend_build_cmd: "{{ project_def.deploy.backend_build_cmd | default('npm run build') }}"
app_backend_migrate_cmd: "{{ env_def.backend_migrate_cmd | default(project_def.deploy.backend_migrate_cmd | default('npm run migrate')) }}"
app_backend_seed_cmd: "{{ env_def.backend_seed_cmd | default(project_def.deploy.backend_seed_cmd | default('')) }}"
app_backend_start_cmd: "{{ project_def.deploy.backend_start_cmd | default('npm run start') }}"
app_frontend_install_cmd: "{{ project_def.deploy.frontend_install_cmd | default('npm ci') }}"
app_frontend_build_cmd: "{{ project_def.deploy.frontend_build_cmd | default('npm run build') }}"

View File

@ -16,7 +16,7 @@
- (project_def.envs | length) > 0
- name: Provision each environment for project
ansible.builtin.include_tasks: provision_one_env.yml
loop: "{{ project_def.envs | dict2items }}"
loop: "{{ project_def.envs | dict2items | selectattr('key', 'in', selected_envs) | list }}"
loop_control:
loop_var: env_item

View File

@ -20,6 +20,12 @@
if (app_project is not defined or app_project | length == 0)
else [app_project]
}}
selected_envs: >-
{{
[app_env]
if (app_env is defined and app_env | length > 0)
else ['dev', 'qa', 'prod']
}}
tasks:
- name: Validate requested project exists

View File

@ -29,6 +29,7 @@ app_frontend_port: 3000
# Commands (Node defaults; override per project as needed)
app_backend_install_cmd: "npm ci"
app_backend_migrate_cmd: "npm run migrate"
app_backend_seed_cmd: ""
app_backend_start_cmd: "npm start"
app_frontend_install_cmd: "npm ci"

View File

@ -10,24 +10,6 @@
group: "{{ app_group }}"
mode: "0755"
- name: Ensure backend directory exists
ansible.builtin.file:
path: "{{ app_backend_dir }}"
state: directory
owner: "{{ app_owner }}"
group: "{{ app_group }}"
mode: "0755"
when: app_enable_backend | bool
- name: Ensure frontend directory exists
ansible.builtin.file:
path: "{{ app_frontend_dir }}"
state: directory
owner: "{{ app_owner }}"
group: "{{ app_group }}"
mode: "0755"
when: app_enable_frontend | bool
- name: Deploy environment file for this env
ansible.builtin.template:
src: env.j2
@ -44,6 +26,16 @@
group: root
mode: "0755"
- name: Run initial deploy (clone, install, build, migrate, seed)
ansible.builtin.command:
cmd: /usr/local/bin/deploy_app.sh
become: true
become_user: "{{ app_owner }}"
args:
chdir: "{{ app_root }}"
register: deploy_result
changed_when: true
- name: Deploy systemd unit for backend
ansible.builtin.template:
src: app-backend.service.j2

View File

@ -11,10 +11,45 @@ ENV_FILE="{{ app_root }}/.env.{{ app_env }}"
echo "[deploy] repo=${REPO_URL} branch=${BRANCH} root=${APP_ROOT}"
# Load env for build/migrate steps (needed for Prisma/Next build)
if [[ -f "${ENV_FILE}" ]]; then
set -a
# shellcheck disable=SC1090
source "${ENV_FILE}"
set +a
fi
if [[ ! -d "${APP_ROOT}/.git" ]]; then
echo "[deploy] cloning repo"
install -d -m 0755 "${APP_ROOT}"
git clone --branch "${BRANCH}" --single-branch "${REPO_URL}" "${APP_ROOT}"
# Preserve existing env files
env_tmp="$(mktemp -d)"
shopt -s nullglob dotglob
for f in "${APP_ROOT}"/.env.*; do
[[ -f "$f" ]] && cp "$f" "${env_tmp}/" || true
done
shopt -u nullglob dotglob
# Clone to temp location
clone_tmp="$(mktemp -d)"
git clone --branch "${BRANCH}" --single-branch "${REPO_URL}" "${clone_tmp}/repo"
# Clean app root (keep directory and .env files)
find "${APP_ROOT}" -mindepth 1 -maxdepth 1 ! -name '.env.*' -exec rm -rf {} + 2>/dev/null || true
# Move cloned repo contents to app root (including hidden files)
shopt -s dotglob
mv "${clone_tmp}/repo"/* "${APP_ROOT}"/ 2>/dev/null || true
shopt -u dotglob
rm -rf "${clone_tmp}"
# Restore env files
shopt -s nullglob
for f in "${env_tmp}"/.env.*; do
[[ -f "$f" ]] && cp "$f" "${APP_ROOT}/" || true
done
shopt -u nullglob
rm -rf "${env_tmp}"
fi
echo "[deploy] syncing branch"
@ -31,8 +66,16 @@ echo "[deploy] backend install"
cd "${BACKEND_DIR}"
{{ app_backend_install_cmd }}
echo "[deploy] backend build"
{{ app_backend_build_cmd }}
echo "[deploy] backend migrations"
{{ app_backend_migrate_cmd }}
{% if app_backend_seed_cmd | default('') | length > 0 %}
echo "[deploy] backend seed"
{{ app_backend_seed_cmd }}
{% endif %}
fi
if [[ "{{ app_enable_frontend | bool }}" == "True" ]]; then
@ -46,10 +89,10 @@ fi
echo "[deploy] restarting services"
{% if app_enable_backend | bool %}
systemctl restart app-backend.service
sudo systemctl restart app-backend.service
{% endif %}
{% if app_enable_frontend | bool %}
systemctl restart app-frontend.service
sudo systemctl restart app-frontend.service
{% endif %}
echo "[deploy] done"

View File

@ -1,11 +1,6 @@
# Ansible-managed environment file for {{ app_env }}
# Loaded by systemd units and deploy script.
# Common
APP_ENV={{ app_env }}
BACKEND_PORT={{ app_backend_port }}
FRONTEND_PORT={{ app_frontend_port }}
{% for k, v in (app_env_vars | default({})).items() %}
{{ k }}={{ v }}
{% endfor %}

View File

@ -12,6 +12,14 @@
name: "{{ base_os_packages }}"
state: present
- name: Ensure /etc/sudoers.d exists
ansible.builtin.file:
path: /etc/sudoers.d
state: directory
owner: root
group: root
mode: "0750"
- name: Ensure app user exists
ansible.builtin.user:
name: "{{ base_os_user }}"
@ -37,6 +45,11 @@
mode: "0440"
when: base_os_passwordless_sudo | bool
- name: Ensure ufw is installed
ansible.builtin.apt:
name: ufw
state: present
- name: Ensure UFW allows SSH
community.general.ufw:
rule: allow