--- # showcase/webapp-web.yml — nginx + PHP-FPM setup for the webapp blueprint # # Discovers the db sibling via Incus API, reads db credentials from the # db instance, deploys a PHP app that shows DB connectivity, and adds # an ACL rule to the db's ACL allowing this web instance on port 3306. # --- Step 1: Find the database instance from the deployment manifest --- - name: Find database instances from deployment ansible.builtin.set_fact: db_instances: >- {{ ffsdn_instances | selectattr('component_name', 'equalto', 'db') | list }} - name: Validate database found ansible.builtin.assert: that: - db_instances | length > 0 fail_msg: >- No db component found in ffsdn_instances. The blueprint must include a db component for the web tier to connect to. - name: Select primary database instance ansible.builtin.set_fact: db_instance: name: "{{ db_instances[0].name }}" ip: "{{ db_instances[0].ip }}" - name: Display database target ansible.builtin.debug: msg: "Using database: {{ db_instance.name }} at {{ db_instance.ip }}" # --- Step 2: Wait for and read database credentials --- - name: Wait for db credentials to be available (up to 5 minutes) ansible.builtin.uri: url: "{{ incus_api }}/1.0/instances/{{ db_instance.name }}/files?path=/etc/webapp-db-creds" method: GET client_cert: "{{ incus_cert }}" client_key: "{{ incus_key }}" validate_certs: false return_content: true status_code: [200, 404] register: db_creds_response until: db_creds_response.status == 200 retries: 30 delay: 10 - name: Parse database credentials ansible.builtin.set_fact: db_host: "{{ db_creds_response.content | regex_search('DB_HOST=(\\S+)', '\\1') | first }}" db_port: "{{ db_creds_response.content | regex_search('DB_PORT=(\\S+)', '\\1') | first }}" db_name: "{{ db_creds_response.content | regex_search('DB_NAME=(\\S+)', '\\1') | first }}" db_user: "{{ db_creds_response.content | regex_search('DB_USER=(\\S+)', '\\1') | first }}" db_pass: "{{ db_creds_response.content | regex_search('DB_PASS=(\\S+)', '\\1') | first }}" - name: Display parsed credentials ansible.builtin.debug: msg: "DB connection: {{ db_user }}@{{ db_host }}:{{ db_port }}/{{ db_name }}" # --- Step 3: Install nginx + PHP-FPM and deploy app --- - name: Generate web setup script ansible.builtin.set_fact: web_setup_script: | #!/bin/bash set -e echo "=== Web tier setup starting ===" export DEBIAN_FRONTEND=noninteractive # Install nginx + PHP-FPM + MySQL extension apt-get install -y -qq nginx php-fpm php-mysql 2>&1 | tail -3 echo "[OK] nginx + PHP-FPM + php-mysql installed" # Find PHP-FPM socket path (varies by PHP version) PHP_SOCK=$(find /run/php/ -name 'php*-fpm.sock' 2>/dev/null | head -1) if [ -z "$PHP_SOCK" ]; then PHP_VERSION=$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;') PHP_SOCK="/run/php/php${PHP_VERSION}-fpm.sock" fi echo "[OK] PHP-FPM socket: $PHP_SOCK" # Configure nginx with PHP cat > /etc/nginx/sites-available/default << NGINX server { listen 80 default_server; root /var/www/html; index index.php index.html; server_name _; location / { try_files \$uri \$uri/ =404; } location ~ \\.php\$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:${PHP_SOCK}; } } NGINX echo "[OK] nginx configured" # Deploy PHP application cat > /var/www/html/index.php << 'PHPAPP' 5] ); $db_status = 'connected'; // Record this visit $stmt = $pdo->prepare('INSERT INTO visits (visitor_ip, visitor_host) VALUES (?, ?)'); $stmt->execute([$_SERVER['REMOTE_ADDR'] ?? 'direct', $instance]); // Count total visits $visit_count = $pdo->query('SELECT COUNT(*) FROM visits')->fetchColumn(); } catch (PDOException $e) { $error = $e->getMessage(); $db_status = 'error'; } ?> Webapp Showcase — <?= htmlspecialchars($instance) ?>

Webapp Showcase

Instance Info

Instance
IP Address
Componentweb

Database Connection

Host:
Database
Total Visits
Error
PHPAPP echo "[OK] PHP application deployed" # Remove default HTML page rm -f /var/www/html/index.html /var/www/html/index.nginx-debian.html # Detect PHP-FPM service name (e.g., php8.2-fpm) PHP_VERSION=$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;') PHP_FPM_SVC="php${PHP_VERSION}-fpm" # Restart services systemctl enable nginx "$PHP_FPM_SVC" systemctl restart "$PHP_FPM_SVC" systemctl restart nginx echo "[OK] Services started (PHP-FPM: $PHP_FPM_SVC)" echo "=== Web tier setup complete ===" - name: Generate web config file content ansible.builtin.set_fact: web_config_content: | DB_HOST={{ db_host }} DB_PORT={{ db_port }} DB_NAME={{ db_name }} DB_USER={{ db_user }} DB_PASS={{ db_pass }} INSTANCE_IP={{ ffsdn_instance_ip | default('unknown') }} - name: Push web config to instance ansible.builtin.uri: url: "{{ incus_api }}/1.0/instances/{{ ffsdn_instance_name }}/files?path=/etc/webapp-web-config" method: POST client_cert: "{{ incus_cert }}" client_key: "{{ incus_key }}" validate_certs: false body: "{{ web_config_content }}" headers: Content-Type: "application/octet-stream" X-Incus-type: "file" X-Incus-mode: "0644" status_code: [200] - name: Push web setup script ansible.builtin.uri: url: "{{ incus_api }}/1.0/instances/{{ ffsdn_instance_name }}/files?path=/tmp/webapp-web-setup.sh" method: POST client_cert: "{{ incus_cert }}" client_key: "{{ incus_key }}" validate_certs: false body: "{{ web_setup_script }}" headers: Content-Type: "application/octet-stream" X-Incus-type: "file" X-Incus-mode: "0755" status_code: [200] - name: Execute web setup script ansible.builtin.uri: url: "{{ incus_api }}/1.0/instances/{{ ffsdn_instance_name }}/exec" method: POST client_cert: "{{ incus_cert }}" client_key: "{{ incus_key }}" validate_certs: false body_format: json body: command: ["/bin/bash", "/tmp/webapp-web-setup.sh"] record-output: true interactive: false wait-for-websocket: false environment: DEBIAN_FRONTEND: "noninteractive" status_code: [202] return_content: true register: web_setup_exec - name: Wait for web setup to complete (up to 5 minutes) ansible.builtin.uri: url: "{{ incus_api }}{{ web_setup_exec.json.operation }}/wait?timeout=300" method: GET client_cert: "{{ incus_cert }}" client_key: "{{ incus_key }}" validate_certs: false return_content: true timeout: 320 register: web_setup_wait failed_when: web_setup_wait.json.metadata.status != "Success" - name: Verify web setup exit code ansible.builtin.assert: that: - web_setup_wait.json.metadata.metadata.return | int == 0 fail_msg: "Web setup exited with code {{ web_setup_wait.json.metadata.metadata.return }}" success_msg: "Web tier setup completed successfully" # --- Step 4: Add ACL rule to allow this web instance to reach the DB --- - name: Build db ACL name ansible.builtin.set_fact: db_acl_name: "52-{{ db_instance.name }}-aether-acl" - name: Read current db ACL ansible.builtin.uri: url: "{{ incus_api }}/1.0/network-acls/{{ db_acl_name }}" method: GET client_cert: "{{ incus_cert }}" client_key: "{{ incus_key }}" validate_certs: false return_content: true status_code: [200, 404] register: db_acl_response - name: Build updated ingress rules (append web access) ansible.builtin.set_fact: updated_ingress: >- {{ (db_acl_response.json.metadata.ingress | default([])) + [{ 'action': 'allow', 'state': 'enabled', 'source': (ffsdn_instance_ip | default('')) + '/32', 'destination': db_instance.ip + '/32', 'protocol': 'tcp', 'destination_port': '3306', 'description': 'Allow ' + ffsdn_instance_name + ' to db on 3306' }] }} when: db_acl_response.status == 200 - name: Update db ACL with web access rule ansible.builtin.uri: url: "{{ incus_api }}/1.0/network-acls/{{ db_acl_name }}" method: PATCH client_cert: "{{ incus_cert }}" client_key: "{{ incus_key }}" validate_certs: false body_format: json body: ingress: "{{ updated_ingress }}" status_code: [200] when: - db_acl_response.status == 200 - updated_ingress is defined - name: Report ACL update ansible.builtin.debug: msg: >- {% if db_acl_response.status == 200 %}ACL '{{ db_acl_name }}' updated: {{ ffsdn_instance_name }} ({{ ffsdn_instance_ip | default('?') }}) → {{ db_instance.name }}:3306 {% else %}ACL '{{ db_acl_name }}' not found — skipping ACL rule (Aether may not have created it yet){% endif %} - name: Clean up setup script ansible.builtin.uri: url: "{{ incus_api }}/1.0/instances/{{ ffsdn_instance_name }}/exec" method: POST client_cert: "{{ incus_cert }}" client_key: "{{ incus_key }}" validate_certs: false body_format: json body: command: ["/bin/rm", "-f", "/tmp/webapp-web-setup.sh"] record-output: false interactive: false wait-for-websocket: false status_code: [202] ignore_errors: true