352 lines
12 KiB
YAML
352 lines
12 KiB
YAML
---
|
|
# 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'
|
|
<?php
|
|
$config = [];
|
|
foreach (file('/etc/webapp-web-config') as $line) {
|
|
$line = trim($line);
|
|
if ($line && strpos($line, '=') !== false) {
|
|
list($key, $value) = explode('=', $line, 2);
|
|
$config[trim($key)] = trim($value);
|
|
}
|
|
}
|
|
|
|
$db_host = $config['DB_HOST'] ?? 'unknown';
|
|
$db_port = $config['DB_PORT'] ?? '3306';
|
|
$db_name = $config['DB_NAME'] ?? 'webapp';
|
|
$db_user = $config['DB_USER'] ?? 'webapp';
|
|
$db_pass = $config['DB_PASS'] ?? '';
|
|
|
|
$instance = gethostname();
|
|
$ip = $config['INSTANCE_IP'] ?? 'unknown';
|
|
$db_status = 'disconnected';
|
|
$visit_count = 0;
|
|
$error = '';
|
|
|
|
try {
|
|
$pdo = new PDO(
|
|
"mysql:host={$db_host};port={$db_port};dbname={$db_name}",
|
|
$db_user, $db_pass,
|
|
[PDO::ATTR_TIMEOUT => 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';
|
|
}
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>Webapp Showcase — <?= htmlspecialchars($instance) ?></title>
|
|
<style>
|
|
body { font-family: -apple-system, sans-serif; max-width: 700px; margin: 40px auto; padding: 0 20px; background: #f5f5f5; }
|
|
.card { background: white; border-radius: 8px; padding: 24px; margin: 16px 0; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
|
h1 { color: #1a1a2e; }
|
|
.status { display: inline-block; padding: 4px 12px; border-radius: 12px; font-size: 14px; font-weight: 600; }
|
|
.connected { background: #d4edda; color: #155724; }
|
|
.error, .disconnected { background: #f8d7da; color: #721c24; }
|
|
table { width: 100%; border-collapse: collapse; }
|
|
td { padding: 8px; border-bottom: 1px solid #eee; }
|
|
td:first-child { font-weight: 600; width: 40%; color: #555; }
|
|
.footer { text-align: center; color: #888; font-size: 13px; margin-top: 24px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>Webapp Showcase</h1>
|
|
<div class="card">
|
|
<h2>Instance Info</h2>
|
|
<table>
|
|
<tr><td>Instance</td><td><?= htmlspecialchars($instance) ?></td></tr>
|
|
<tr><td>IP Address</td><td><?= htmlspecialchars($ip) ?></td></tr>
|
|
<tr><td>Component</td><td>web</td></tr>
|
|
</table>
|
|
</div>
|
|
<div class="card">
|
|
<h2>Database Connection <span class="status <?= $db_status ?>"><?= $db_status ?></span></h2>
|
|
<table>
|
|
<tr><td>Host</td><td><?= htmlspecialchars($db_host) ?>:<?= htmlspecialchars($db_port) ?></td></tr>
|
|
<tr><td>Database</td><td><?= htmlspecialchars($db_name) ?></td></tr>
|
|
<tr><td>Total Visits</td><td><?= $visit_count ?></td></tr>
|
|
<?php if ($error): ?>
|
|
<tr><td>Error</td><td style="color:#721c24"><?= htmlspecialchars($error) ?></td></tr>
|
|
<?php endif; ?>
|
|
</table>
|
|
</div>
|
|
<div class="footer">
|
|
Deployed by AWX via Aether — <?= date('Y-m-d H:i:s T') ?>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
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
|