--- # Usage: ansible-playbook -i 'root@,' deploy/ansible/deploy.yml # Pre: droplet cloud-init complete (.bootstrap-complete marker present) - name: Deploy aibook-fun app layer hosts: all gather_facts: false become: true vars: app_dir: /opt/aibook_fun repo_dir: /opt/aibook_fun/repo repo_url: "https://git.weown.tools/WeOwnAI/aibook-fun.git" repo_branch: main image_tag: "aibook-fun-marketing:v1" pre_tasks: - name: Verify droplet has completed cloud-init bootstrap ansible.builtin.stat: path: "{{ app_dir }}/.bootstrap-complete" register: bootstrap_marker - name: Fail clearly if bootstrap is not complete ansible.builtin.fail: msg: >- {{ app_dir }}/.bootstrap-complete is missing — droplet has not finished cloud-init. Wait a few minutes after `tofu apply` and retry. Check /var/log/cloud-init-output.log on the droplet. when: not bootstrap_marker.stat.exists - name: Ensure app dir exists ansible.builtin.file: path: "{{ app_dir }}" state: directory owner: root group: root mode: "0755" tasks: # ─── Clone / update repo on droplet ──────────────────────────────────── - name: Clone (or update) aibook-fun repo on droplet ansible.builtin.git: repo: "{{ repo_url }}" dest: "{{ repo_dir }}" version: "{{ repo_branch }}" force: true register: git_clone notify: Rebuild image # ─── Upload Dockerfile (deploy/ scaffold not yet pushed to git.weown.tools) ─ - name: Upload Dockerfile to droplet ansible.builtin.copy: src: ../docker/Dockerfile dest: "{{ app_dir }}/Dockerfile" owner: root group: root mode: "0644" # ─── Build docker image locally on droplet (unconditional — docker layer cache handles no-ops) ─ - name: Build Next.js docker image locally ansible.builtin.shell: | set -euo pipefail docker build -t {{ image_tag }} -f {{ app_dir }}/Dockerfile {{ repo_dir }}/marketing-site/ args: executable: /bin/bash register: image_build changed_when: "'writing image' in image_build.stderr" notify: Reconcile docker compose stack # ─── Upload compose + Caddyfile ──────────────────────────────────────── - name: Upload compose.yaml ansible.builtin.copy: src: ../docker/compose.prod.yaml dest: "{{ app_dir }}/compose.yaml" owner: root group: root mode: "0644" notify: Reconcile docker compose stack - name: Upload Caddyfile ansible.builtin.copy: src: ../docker/Caddyfile dest: "{{ app_dir }}/Caddyfile" owner: root group: root mode: "0644" notify: Reconcile docker compose stack - name: Ensure caddy log dir is writable ansible.builtin.file: path: /var/log/caddy state: directory owner: root group: root mode: "0777" - name: Ensure Caddy Docker volumes are writable (caddy runs as UID 1000) ansible.builtin.shell: | set -euo pipefail for suffix in caddy_data caddy_config; do path="/var/lib/docker/volumes/aibook_fun_${suffix}/_data" if [ -d "$path" ]; then chown -R 1000:1000 "$path" fi done args: executable: /bin/bash changed_when: false # ─── Ensure compose stack is running (idempotent) ────────────────────── - name: Ensure compose stack is running (idempotent — `up -d` no-op if unchanged) ansible.builtin.shell: | set -euo pipefail cd {{ app_dir }} docker compose up -d --remove-orphans args: executable: /bin/bash register: compose_up changed_when: "'Started' in compose_up.stderr or 'Created' in compose_up.stderr or 'Recreated' in compose_up.stderr" - name: Wait for web app to respond ansible.builtin.uri: url: http://127.0.0.1:3000/ status_code: [200] register: health_probe retries: 20 delay: 5 until: health_probe.status == 200 - name: Report success ansible.builtin.debug: msg: "aibook-fun app layer deployed. URL: https://aibook.fun" handlers: - name: Rebuild image ansible.builtin.shell: | set -euo pipefail cd {{ repo_dir }} docker build -t {{ image_tag }} -f {{ repo_dir }}/deploy/docker/Dockerfile {{ repo_dir }}/marketing-site/ args: executable: /bin/bash changed_when: false - name: Reconcile docker compose stack ansible.builtin.shell: | set -euo pipefail cd {{ app_dir }} docker compose up -d --force-recreate --remove-orphans args: executable: /bin/bash changed_when: false