295 lines
7.8 KiB
YAML
295 lines
7.8 KiB
YAML
---
|
|
# The Door — Ansible Playbook
|
|
# VPS provisioning for the crisis front door
|
|
#
|
|
# Usage:
|
|
# cd deploy && ansible-playbook -i inventory.ini playbook.yml
|
|
#
|
|
# This playbook is IDEMPOTENT — safe to run repeatedly.
|
|
# It handles: swap, nginx, SSL, firewall, site deployment.
|
|
|
|
- name: "The Door — VPS Provisioning"
|
|
hosts: the_door
|
|
become: true
|
|
vars:
|
|
domain: "alexanderwhitestone.com"
|
|
domain_www: "www.alexanderwhitestone.com"
|
|
site_root: "/var/www/the-door"
|
|
swap_size: "2G"
|
|
swap_file: "/swapfile"
|
|
hermes_port: 8644
|
|
deploy_dir: "/opt/the-door"
|
|
|
|
tasks:
|
|
# ================================================================
|
|
# PHASE 1: System — swap, updates, packages
|
|
# ================================================================
|
|
|
|
- name: "[swap] Check if swapfile exists"
|
|
stat:
|
|
path: "{{ swap_file }}"
|
|
register: swap_stat
|
|
|
|
- name: "[swap] Create swapfile"
|
|
command: fallocate -l {{ swap_size }} {{ swap_file }}
|
|
when: not swap_stat.stat.exists
|
|
|
|
- name: "[swap] Set permissions"
|
|
file:
|
|
path: "{{ swap_file }}"
|
|
mode: "0600"
|
|
when: not swap_stat.stat.exists
|
|
|
|
- name: "[swap] Make swap"
|
|
command: mkswap {{ swap_file }}
|
|
when: not swap_stat.stat.exists
|
|
|
|
- name: "[swap] Enable swap"
|
|
command: swapon {{ swap_file }}
|
|
when: not swap_stat.stat.exists
|
|
|
|
- name: "[swap] Add to fstab"
|
|
lineinfile:
|
|
path: /etc/fstab
|
|
line: "{{ swap_file }} none swap sw 0 0"
|
|
state: present
|
|
when: not swap_stat.stat.exists
|
|
|
|
- name: "[apt] Update cache"
|
|
apt:
|
|
update_cache: yes
|
|
cache_valid_time: 3600
|
|
|
|
- name: "[apt] Install packages"
|
|
apt:
|
|
name:
|
|
- nginx
|
|
- certbot
|
|
- python3-certbot-nginx
|
|
- ufw
|
|
- curl
|
|
state: present
|
|
|
|
# ================================================================
|
|
# PHASE 2: Site files — copy static assets
|
|
# ================================================================
|
|
|
|
- name: "[site] Create webroot"
|
|
file:
|
|
path: "{{ site_root }}"
|
|
state: directory
|
|
owner: www-data
|
|
group: www-data
|
|
mode: "0755"
|
|
|
|
- name: "[site] Copy index.html"
|
|
copy:
|
|
src: "{{ playbook_dir }}/../index.html"
|
|
dest: "{{ site_root }}/index.html"
|
|
owner: www-data
|
|
group: www-data
|
|
mode: "0644"
|
|
notify: reload nginx
|
|
|
|
- name: "[site] Copy manifest.json"
|
|
copy:
|
|
src: "{{ playbook_dir }}/../manifest.json"
|
|
dest: "{{ site_root }}/manifest.json"
|
|
owner: www-data
|
|
group: www-data
|
|
mode: "0644"
|
|
notify: reload nginx
|
|
|
|
- name: "[site] Copy service worker"
|
|
copy:
|
|
src: "{{ playbook_dir }}/../sw.js"
|
|
dest: "{{ site_root }}/sw.js"
|
|
owner: www-data
|
|
group: www-data
|
|
mode: "0644"
|
|
notify: reload nginx
|
|
|
|
- name: "[site] Copy system prompt"
|
|
copy:
|
|
src: "{{ playbook_dir }}/../system-prompt.txt"
|
|
dest: "{{ site_root }}/system-prompt.txt"
|
|
owner: www-data
|
|
group: www-data
|
|
mode: "0644"
|
|
|
|
- name: "[site] Copy about page"
|
|
copy:
|
|
src: "{{ playbook_dir }}/../about.html"
|
|
dest: "{{ site_root }}/about.html"
|
|
owner: www-data
|
|
group: www-data
|
|
mode: "0644"
|
|
notify: reload nginx
|
|
|
|
- name: "[site] Copy testimony page"
|
|
copy:
|
|
src: "{{ playbook_dir }}/../testimony.html"
|
|
dest: "{{ site_root }}/testimony.html"
|
|
owner: www-data
|
|
group: www-data
|
|
mode: "0644"
|
|
notify: reload nginx
|
|
|
|
# ================================================================
|
|
# PHASE 3: nginx — config, sites, rate limiting
|
|
# ================================================================
|
|
|
|
- name: "[nginx] Ensure sites-available dir"
|
|
file:
|
|
path: /etc/nginx/sites-available
|
|
state: directory
|
|
|
|
- name: "[nginx] Ensure sites-enabled dir"
|
|
file:
|
|
path: /etc/nginx/sites-enabled
|
|
state: directory
|
|
|
|
- name: "[nginx] Deploy site config"
|
|
copy:
|
|
src: "{{ playbook_dir }}/nginx.conf"
|
|
dest: /etc/nginx/sites-available/the-door
|
|
owner: root
|
|
group: root
|
|
mode: "0644"
|
|
notify: reload nginx
|
|
|
|
- name: "[nginx] Enable site"
|
|
file:
|
|
src: /etc/nginx/sites-available/the-door
|
|
dest: /etc/nginx/sites-enabled/the-door
|
|
state: link
|
|
notify: reload nginx
|
|
|
|
- name: "[nginx] Remove default site"
|
|
file:
|
|
path: /etc/nginx/sites-enabled/default
|
|
state: absent
|
|
notify: reload nginx
|
|
|
|
- name: "[nginx] Add rate limit zone to main config"
|
|
lineinfile:
|
|
path: /etc/nginx/nginx.conf
|
|
insertafter: "http {"
|
|
line: " limit_req_zone $binary_remote_addr zone=the_door_api:10m rate=10r/m;"
|
|
notify: reload nginx
|
|
|
|
- name: "[nginx] Test config"
|
|
command: nginx -t
|
|
changed_when: false
|
|
|
|
- name: "[nginx] Ensure service is running"
|
|
service:
|
|
name: nginx
|
|
state: started
|
|
enabled: yes
|
|
|
|
# ================================================================
|
|
# PHASE 4: Firewall — UFW
|
|
# ================================================================
|
|
|
|
- name: "[ufw] Allow SSH"
|
|
ufw:
|
|
rule: allow
|
|
port: "22"
|
|
proto: tcp
|
|
|
|
- name: "[ufw] Allow HTTP"
|
|
ufw:
|
|
rule: allow
|
|
port: "80"
|
|
proto: tcp
|
|
|
|
- name: "[ufw] Allow HTTPS"
|
|
ufw:
|
|
rule: allow
|
|
port: "443"
|
|
proto: tcp
|
|
|
|
- name: "[ufw] Set default deny incoming"
|
|
ufw:
|
|
direction: incoming
|
|
policy: deny
|
|
|
|
- name: "[ufw] Set default allow outgoing"
|
|
ufw:
|
|
direction: outgoing
|
|
policy: allow
|
|
|
|
- name: "[ufw] Enable firewall"
|
|
ufw:
|
|
state: enabled
|
|
|
|
# ================================================================
|
|
# PHASE 5: SSL — certbot (manual trigger recommended)
|
|
# ================================================================
|
|
|
|
- name: "[ssl] Check if cert exists"
|
|
stat:
|
|
path: "/etc/letsencrypt/live/{{ domain }}/fullchain.pem"
|
|
register: ssl_cert
|
|
|
|
- name: "[ssl] Obtain certificate (if DNS is pointed)"
|
|
command: >
|
|
certbot --nginx
|
|
-d {{ domain }}
|
|
-d {{ domain_www }}
|
|
--non-interactive
|
|
--agree-tos
|
|
--register-unsafely-without-email
|
|
when: not ssl_cert.stat.exists
|
|
register: certbot_result
|
|
ignore_errors: true
|
|
|
|
- name: "[ssl] Certbot result"
|
|
debug:
|
|
msg: "{{ 'SSL cert obtained' if certbot_result.rc == 0 else 'SSL cert needs manual setup — point DNS first, then run: certbot --nginx -d ' + domain + ' -d ' + domain_www }}"
|
|
when: not ssl_cert.stat.exists
|
|
|
|
# ================================================================
|
|
# PHASE 6: Deploy directory + deploy script
|
|
# ================================================================
|
|
|
|
- name: "[deploy] Create deploy directory"
|
|
file:
|
|
path: "{{ deploy_dir }}"
|
|
state: directory
|
|
owner: root
|
|
group: root
|
|
mode: "0755"
|
|
|
|
- name: "[deploy] Copy deploy script"
|
|
copy:
|
|
src: "{{ playbook_dir }}/deploy.sh"
|
|
dest: "{{ deploy_dir }}/deploy.sh"
|
|
owner: root
|
|
group: root
|
|
mode: "0755"
|
|
|
|
- name: "[deploy] Copy system-prompt.txt"
|
|
copy:
|
|
src: "{{ playbook_dir }}/../system-prompt.txt"
|
|
dest: "{{ deploy_dir }}/system-prompt.txt"
|
|
owner: root
|
|
group: root
|
|
mode: "0644"
|
|
|
|
# ================================================================
|
|
# HANDLERS
|
|
# ================================================================
|
|
|
|
handlers:
|
|
- name: reload nginx
|
|
service:
|
|
name: nginx
|
|
state: reloaded
|
|
|
|
- name: restart nginx
|
|
service:
|
|
name: nginx
|
|
state: restarted
|