From 78da6b29949598fd6466b44d68e6aed87bfff3c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20Berry?= Date: Tue, 28 Oct 2025 14:35:40 +0100 Subject: [PATCH] Initial commit --- README.md | 129 ++++++++++++++++++++++++++++++++++++ manifests/app.pp | 85 ++++++++++++++++++++++++ manifests/config.pp | 36 ++++++++++ manifests/init.pp | 18 +++++ manifests/install.pp | 36 ++++++++++ manifests/params.pp | 14 ++++ manifests/service.pp | 15 +++++ manifests/vhost.pp | 40 +++++++++++ metadata.json | 50 ++++++++++++++ templates/app_caddyfile.epp | 24 +++++++ templates/vhost.epp | 16 +++++ 11 files changed, 463 insertions(+) create mode 100644 README.md create mode 100644 manifests/app.pp create mode 100644 manifests/config.pp create mode 100644 manifests/init.pp create mode 100644 manifests/install.pp create mode 100644 manifests/params.pp create mode 100644 manifests/service.pp create mode 100644 manifests/vhost.pp create mode 100644 metadata.json create mode 100644 templates/app_caddyfile.epp create mode 100644 templates/vhost.epp diff --git a/README.md b/README.md new file mode 100644 index 0000000..2d5f05f --- /dev/null +++ b/README.md @@ -0,0 +1,129 @@ +# Puppet FrankenPHP Module + +## Overview + +This Puppet module installs and manages **FrankenPHP** on Debian-based systems (Debian, Ubuntu) using the official `.deb` package. + +It is designed for a **secure, multi-tenant architecture** by managing two distinct components: +1. **FrankenPHP (Proxy)**: A main Caddy/FrankenPHP service, installed via `.deb` and managed by `systemd`, which acts as a reverse proxy. +2. **Isolated Applications**: Individual PHP applications, each running in its own `frankenphp` process under a dedicated system user. These processes are managed by **Supervisor** for process and user isolation. + +## Prerequisites + +This module has external dependencies (see `metadata.json`): +* `puppetlabs/concat`: Used to assemble the main `Caddyfile` from fragments. +* `puppetlabs/stdlib`: For data types (`Stdlib::Absolutepath`). +* `ajcrowe/supervisord`: Used to manage the isolated application processes. + +You **must** ensure that Supervisor is installed on the target node. This module only manages *configurations* for Supervisor, not its installation. + +```puppet +# In your base profile or site.pp +include 'supervisor' +```` + +## Usage + +The architecture is deployed in two steps for each website: + +1. Declare the isolated application (`frankenphp::app`). +2. Declare the public-facing vhost that points to it (`frankenphp::vhost`). + +### 1\. Base Installation + +Simply include the main class. This installs the FrankenPHP binary and configures the proxy service (which is empty by default). + +```puppet +include 'frankenphp' +``` + +### 2\. Defining an Isolated Application (`frankenphp::app`) + +This defined type creates a dedicated user, a root directory, a specific Caddyfile for the app, and a Supervisor service to run it on a local port. + +```puppet +# Declare a 'blog-app' application +frankenphp::app { 'blog-app': + ensure => present, + root_dir => '/var/www/blog', + user => 'bloguser', + listen_port => '9010', # Ensure this port is free +} +``` + +This code will: + + * Create the `bloguser` user. + * Create the `/var/www/blog` directory (owned by `bloguser`). + * Create a Caddyfile for this app (listening on `localhost:9010`). + * Create the `/etc/supervisor/conf.d/frankenphp-blog-app.conf` file to launch this process as the `bloguser`. + +### 3\. Exposing the Application (`frankenphp::vhost`) + +This adds an entry to the main proxy `Caddyfile` to route public traffic to the isolated application. + +```puppet +# Create the public vhost for blog.example.com +frankenphp::vhost { 'blog.example.com': + ensure => present, + mode => 'proxy', + proxy_target => 'localhost:9010', # Must match the app's listen_port + extra_config => @(EOT) + # Caddy will handle HTTPS automatically. + # We add the Host header for the proxy. + header_up Host {host} + EOT + , + # Ensure the proxy isn't defined until the app is managed + require => Frankenphp::App['blog-app'], +} +``` + +## Classes and Defines + +### Class: `frankenphp` + +The main class that orchestrates installation (`frankenphp::install`), proxy configuration (`frankenphp::config`), and the main service (`frankenphp::service`). + +**Parameters:** + + * `version` (String): The FrankenPHP version to download (e.g., '1.9.1'). + * `service_ensure` (String): The state of the main service (default: 'running'). + * `service_enable` (Boolean): Enable the main service on boot (default: true). + +### Define: `frankenphp::app` + +Manages a single isolated PHP application. + +**Parameters:** + + * `root_dir` (Stdlib::Absolutepath): The application's document root. + * `user` (String): The username to create for this application. + * `listen_port` (String): The local port the application will listen on (e.g., '9010'). + * `group` (String): The group for the user (default: `$user`). + * `ensure` (Enum['present', 'absent']): Whether the application should exist (default: 'present'). + +### Define: `frankenphp::vhost` + +Manages an entry (a "site") in the main proxy `Caddyfile`. + +**Parameters:** + + * `server_name` (String): The vhost name (default: `$title`, e.g., 'https://www.google.com/url?sa=E\&source=gmail\&q=blog.example.com'). + * `mode` (Enum['proxy', 'fastcgi', 'php\_server']): How Caddy should handle this vhost. **For this architecture, always use 'proxy'**. + * `proxy_target` (Optional[String]): The reverse proxy target (e.g., 'localhost:9010'). + * `fpm_socket` (Optional[String]): (For `fastcgi` mode) Path to the FPM socket. + * `root_dir` (Stdlib::Absolutepath): (For `php_server`/`fastcgi` modes) Document root. + * `extra_config` (String): A raw string of extra Caddy directives for this vhost (for logs, headers, etc.). + * `ensure` (Enum['present', 'absent']): Whether the vhost should exist (default: 'present'). + +## Limitations + + * This module only installs FrankenPHP via the `.deb` package. It does not support other installation methods or operating systems. + * The installation of the `supervisor` service itself is not handled. You must install it via another module or `package` resource. + +## License + +Apache 2.0 + +``` diff --git a/manifests/app.pp b/manifests/app.pp new file mode 100644 index 0000000..12f7c18 --- /dev/null +++ b/manifests/app.pp @@ -0,0 +1,85 @@ +# modules/frankenphp/manifests/app.pp + +define frankenphp::app ( + Stdlib::Absolutepath $root_dir, + String $user, + String $listen_port, + String $group = $user, + Stdlib::Absolutepath $docs_root = '/var/www', + String $app_caddyfile_dir = '/etc/frankenphp/sites.d', + String $supervisor_conf_dir = '/etc/supervisor/conf.d', + Boolean $managed_root_dir = true, + Enum['present', 'absent'] $ensure = 'present', +) { + + $app_name = $title + $app_caddyfile = "${app_caddyfile_dir}/${app_name}.Caddyfile" + $supervisor_conf = "${supervisor_conf_dir}/frankenphp-${app_name}.conf" + + exec { "refresh-frankenphp-${app_name}": + command => "supervisorctl restart frankenphp-${app_name}", + path => ['/usr/bin', '/bin'], + refreshonly => true, + } + + if $ensure == 'present' { + # Users + ensure_resource('group', $group, { ensure => 'present' }) + ensure_resource('user', $user, { + ensure => 'present', + gid => $group, + shell => '/bin/false', + home => "${docs_root}/${user}", + system => true, + require => Group[$group], + }) + + # Directories + ensure_resource('file', $app_caddyfile_dir, { ensure => 'directory' }) + if ! $managed_root_dir { + ensure_resource('file', $root_dir, { + ensure => 'directory', + owner => $user, + group => $group, + mode => '0755', + require => User[$user], + }) + } + + # Caddyfile + $caddy_content = epp('frankenphp/app_caddyfile.epp', { + listen_port => $listen_port, + root_dir => $root_dir, + }) + + file { $app_caddyfile: + ensure => 'file', + owner => 'root', + group => 'root', + mode => '0644', + content => $caddy_content, + notify => Exec["refresh-frankenphp-${app_name}"], + } + + # Supervisor + supervisord::program { "frankenphp-${app_name}": + command => "/usr/bin/frankenphp run --config ${app_caddyfile}", + priority => '100', + user => $user, + autorestart => true, + autostart => true, + startretries => 20, + program_environment => { + 'PATH' => '/bin:/sbin:/usr/bin:/usr/sbin', + }, + require => File[$app_caddyfile] + } + + } else { + file { $app_caddyfile: ensure => 'absent' } + file { $supervisor_conf: + ensure => 'absent', + notify => Service['supervisor'], + } + } +} diff --git a/manifests/config.pp b/manifests/config.pp new file mode 100644 index 0000000..e2bf826 --- /dev/null +++ b/manifests/config.pp @@ -0,0 +1,36 @@ +# modules/frankenphp/manifests/config.pp + +class frankenphp::config ( + $caddyfile_path = $frankenphp::params::caddyfile_path, + $service_name = $frankenphp::params::service_name, +) inherits frankenphp::params { + + $caddyfile_dir = dirname($caddyfile_path) + ensure_resource('file', $caddyfile_dir, { + ensure => 'directory', + owner => 'root', + group => 'root', + mode => '0755', + }) + + concat { $caddyfile_path: + owner => 'root', + group => 'root', + mode => '0644', + notify => Service[$service_name], + require => File[$caddyfile_dir], + } + + concat::fragment { 'caddyfile-global-options': + target => $caddyfile_path, + order => '01', + content => @(EOT) + # Options globales Caddy (email, etc.) + # { + # admin off + # email admin@example.com + # } + + EOT + } +} diff --git a/manifests/init.pp b/manifests/init.pp new file mode 100644 index 0000000..1fa627c --- /dev/null +++ b/manifests/init.pp @@ -0,0 +1,18 @@ +# modules/frankenphp/manifests/init.pp + +class frankenphp ( + String $version = $frankenphp::params::version, + String $service_ensure = 'running', + Boolean $service_enable = true, +) inherits frankenphp::params { + + require supervisord + + contain 'frankenphp::install' + contain 'frankenphp::config' + contain 'frankenphp::service' + + Class['frankenphp::install'] -> + Class['frankenphp::config'] ~> + Class['frankenphp::service'] +} diff --git a/manifests/install.pp b/manifests/install.pp new file mode 100644 index 0000000..5b261d7 --- /dev/null +++ b/manifests/install.pp @@ -0,0 +1,36 @@ +# modules/frankenphp/manifests/install.pp + +class frankenphp::install ( + $version = $frankenphp::params::version, + $package_name = $frankenphp::params::package_name, + $download_url = $frankenphp::params::download_url, + $local_deb_path = $frankenphp::params::local_deb_path, +) inherits frankenphp::params { + + if ! defined(Package['wget']) { + package { 'wget': + ensure => installed, + } + } + + exec { "download-frankenphp-${version}": + command => "/usr/bin/wget -q '${download_url}' -O '${local_deb_path}'", + creates => $local_deb_path, + path => ['/usr/bin', '/bin'], + require => Package['wget'], + } + + package { $package_name: + ensure => 'installed', + provider => 'dpkg', + source => $local_deb_path, + require => Exec["download-frankenphp-${version}"], + notify => Exec["cleanup-frankenphp-deb-${version}"], + } + + exec { "cleanup-frankenphp-deb-${version}": + command => "/bin/rm -f '${local_deb_path}'", + path => ['/bin'], + refreshonly => true, + } +} diff --git a/manifests/params.pp b/manifests/params.pp new file mode 100644 index 0000000..9514474 --- /dev/null +++ b/manifests/params.pp @@ -0,0 +1,14 @@ +# modules/frankenphp/manifests/params.pp + +class frankenphp::params { + $version = '1.9.1' + $package_name = 'frankenphp' + $service_name = 'frankenphp' + $caddyfile_path = '/etc/frankenphp/Caddyfile' + $download_dir = '/tmp' + $arch = $facts['architecture'] + $deb_filename = "frankenphp_${version}-1_${arch}.deb" + $local_deb_path = "${download_dir}/${deb_filename}" + $download_base_url = 'https://github.com/php/frankenphp/releases/download' + $download_url = "${download_base_url}/v${version}/${deb_filename}" +} diff --git a/manifests/service.pp b/manifests/service.pp new file mode 100644 index 0000000..95eb664 --- /dev/null +++ b/manifests/service.pp @@ -0,0 +1,15 @@ +# modules/frankenphp/manifests/service.pp + +class frankenphp::service ( + $service_name = $frankenphp::params::service_name, + $package_name = $frankenphp::params::package_name, + $service_ensure = 'running', + $service_enable = true, +) inherits frankenphp::params { + + service { $service_name: + ensure => $service_ensure, + enable => $service_enable, + subscribe => Package[$package_name], + } +} diff --git a/manifests/vhost.pp b/manifests/vhost.pp new file mode 100644 index 0000000..6b861dd --- /dev/null +++ b/manifests/vhost.pp @@ -0,0 +1,40 @@ +# modules/frankenphp/manifests/vhost.pp + +define frankenphp::vhost ( + String $server_name, + Stdlib::Absolutepath $root_dir = '/var/www/default', + Optional[String] $proxy_target = undef, + Optional[String] $fpm_socket = undef, + Enum['php_server', 'proxy', 'fastcgi'] $mode = 'php_server', + String $extra_config = '', + Enum['present', 'absent'] $ensure = 'present', + String $order = '10', +) { + + $caddyfile_path = $frankenphp::params::caddyfile_path + + # Valider que les bons paramètres sont passés + # if $mode == 'php_server' and $root_dir == '/var/www/default' { + # } + if $mode == 'proxy' and !$proxy_target { + fail('Le mode proxy nécessite un $proxy_target (ex: "localhost:9001")') + } + if $mode == 'fastcgi' and !$fpm_socket { + fail('Le mode fastcgi nécessite un $fpm_socket (ex: "unix//run/php/php.sock")') + } + + $vhost_content = epp('frankenphp/vhost.epp', { + server_name => $server_name, + root_dir => $root_dir, + mode => $mode, + proxy_target => $proxy_target, + fpm_socket => $fpm_socket, + extra_config => $extra_config, + }) + + concat::fragment { "caddyfile-vhost-${server_name}": + target => $caddyfile_path, + order => $order, + content => $vhost_content, + } +} diff --git a/metadata.json b/metadata.json new file mode 100644 index 0000000..47d7acc --- /dev/null +++ b/metadata.json @@ -0,0 +1,50 @@ +{ + "name": "mezcalito-frankenphp", + "version": "0.1.0", + "author": "Léo Berry", + "summary": "Installe et configure FrankenPHP depuis un .deb et gère des applications isolées.", + "license": "Apache-2.0", + "source": "https://github.com/example/puppet-frankenphp", + "project_page": "https://github.com/example/puppet-frankenphp", + "issues_url": "https://github.com/example/puppet-frankenphp/issues", + "dependencies": [ + { + "name": "puppetlabs/concat", + "version_requirement": ">= 6.0.0 < 10.0.0" + }, + { + "name": "puppetlabs/stdlib", + "version_requirement": ">= 6.0.0 < 10.0.0" + }, + { + "name": "ajcrowe/supervisord", + "version_requirement": ">= 0.5.0 < 1.0.0" + } + ], + "operatingsystem_support": [ + { + "operatingsystem": "Debian", + "operatingsystemrelease": [ + "10", + "11", + "12" + ] + }, + { + "operatingsystem": "Ubuntu", + "operatingsystemrelease": [ + "20.04", + "22.04" + ] + } + ], + "requirements": [ + { + "name": "puppet", + "version_requirement": ">= 6.0.0 < 9.0.0" + } + ], + "pdk-version": "3.0.0", + "template-url": "pdk-default#3.0.0", + "template-ref": "tags/3.0.0-0-g069365d" +} diff --git a/templates/app_caddyfile.epp b/templates/app_caddyfile.epp new file mode 100644 index 0000000..2dba6f6 --- /dev/null +++ b/templates/app_caddyfile.epp @@ -0,0 +1,24 @@ +# Caddyfile pour l'application écoutant sur localhost +{ + admin off +} + +:<%= $listen_port %> { + root * <%= $root_dir %> + + encode zstd br gzip + + # Clear Headers + header { + -Server + -X-Powered-By + } + + # Logs + log { + output stdout + } + + file_server + php_server +} diff --git a/templates/vhost.epp b/templates/vhost.epp new file mode 100644 index 0000000..91df6ae --- /dev/null +++ b/templates/vhost.epp @@ -0,0 +1,16 @@ +<%= $server_name %> { + <%- if $mode == 'php_server' { -%> + # Mode: php_server (utilisateur partagé) + root * <%= $root_dir %> + file_server + php_server + <%- } elsif $mode == 'fastcgi' { -%> + # Mode: php_fastcgi (pool FPM, utilisateur isolé) + root * <%= $root_dir %> + file_server + php_fastcgi <%= $fpm_socket %> + <%- } elsif $mode == 'proxy' { -%> + # Mode: reverse_proxy (processus FrankenPHP isolé) + reverse_proxy <%= $proxy_target %> + <%- } -%> +}