---
# 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 | = htmlspecialchars($instance) ?> |
| IP Address | = htmlspecialchars($ip) ?> |
| Component | web |
Database Connection = $db_status ?>
| Host | = htmlspecialchars($db_host) ?>:= htmlspecialchars($db_port) ?> |
| Database | = htmlspecialchars($db_name) ?> |
| Total Visits | = $visit_count ?> |
| Error | = htmlspecialchars($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