puppet-mastodon/manifests/init.pp

473 lines
16 KiB
Puppet

# Class to install a Mastodon instance
#
# [*ensure*]
# String defining if is present or absent
#
# [*hostname*]
# String with the full qualified hostname of the instance. There must be a DNS record for it.
#
# [*mastodon_home*]
# String path to Mastodon user home directory. Default /opt/mastodon
#
# [*mastodon_version*]
# String with Mastodon version (code tag) to install. Default: v4.2.1
#
# [*mastodon_user*]
# String with the system user name for mastodon. Default: mastodon
#
# [*mastodon_group*]
# String with the system group name for mastodon. Default: mastodon
#
# [*ruby_version*]
# String with the ruby version to use. Default: 3.2.2
#
# [*config*]
# Hash with the configuration to store in .env.production
#
# [*db_host*]
# String with database host name or socket.
#
# [*db_name*]
# String with database name.
#
# [*db_user*]
# String with database user name.
#
# [*db_password*]
# String with database user password.
#
# [*db_port*]
# Integer with database port.
#
# [*secret_key_base*]
# String with secret key base.
#
# [*otp_secret*]
# String with OTP (One-Time-Password) secret.
#
# [*vapid_private_key*]
# String with VAPID private key
#
# [*vapid_public_key*]
# String with VAPID public key
#
# [*users*]
# List of hashes with users information.
#
class mastodon (
String $ensure = 'present',
String $hostname = 'mastodon.example.org',
String $mastodon_home = '/opt/mastodon',
String $db_host = '/var/run/postgresql',
String $db_name = 'mastodon',
String $db_user = 'mastodon',
String $db_password = 'S3cr3TP4ssw0rd',
Integer $db_port = 5432,
String $mastodon_version = 'v4.2.1',
String $ruby_version = '3.2.2',
String $mastodon_user = 'mastodon',
String $mastodon_group = 'mastodon',
String $secret_key_base = 'S3cr3tK3i',
String $otp_secret = '0tpS3cr3t',
String $vapid_private_key = 'S3cr3tK3i',
String $vapid_public_key = 'S3cr3tK3i',
Hash $config = {
'LOCAL_DOMAIN' => 'example.com',
'REDIS_HOST' => '127.0.0.1',
'REDIS_PORT' => 6379,
'ES_ENABLED' => 'false',
'ES_HOST' => 'localhost',
'ES_PORT' => 9200,
'ES_USER' => 'elastic',
'ES_PASS' => 'password',
'SMTP_SERVER' => '',
'SMTP_PORT' => 587,
'SMTP_LOGIN' => '',
'SMTP_PASSWORD' => '',
'SMTP_FROM_ADDRESS' => 'notifications@example.com',
'S3_ENABLED' => 'false',
'S3_BUCKET' => 'files.example.com',
'AWS_ACCESS_KEY_ID' => '',
'AWS_SECRET_ACCESS_KEY' => '',
'S3_ALIAS_HOST' => 'files.example.com',
'IP_RETENTION_PERIOD' => 31556952,
'SESSION_RETENTION_PERIOD' => 31556952,
},
Array $users = [],
) {
case $ensure {
default: {
$package_ensure = 'installed'
$directory_ensure = 'directory'
$link_ensure = 'link'
$service_ensure = 'running'
$file_ensure = 'present'
}
/^(absent|delete|uninstall|remove|unregister)$/: {
$package_ensure = 'absent'
$directory_ensure = 'absent'
$link_ensure = 'absent'
$file_ensure = 'absent'
$service_ensure = 'stopped'
$cron_ensure = 'absent'
}
}
$packages = [
'apt-transport-https',
'autoconf',
'bison',
'build-essential',
'ca-certificates',
'certbot',
'ffmpeg',
'file',
'g++',
'gcc',
'git-core',
'gnupg',
'imagemagick',
'libffi-dev',
'libgdbm-dev',
'libicu-dev',
'libidn11-dev',
'libjemalloc-dev',
'libncurses5-dev',
'libpq-dev',
'libprotobuf-dev',
'libreadline6-dev',
'libssl-dev',
'libxml2-dev',
'libxslt1-dev',
'libyaml-dev',
'lsb-release',
'nginx',
'pkg-config',
# 'postgresql-contrib',
'protobuf-compiler',
'python3-certbot-apache',
# 'redis-tools',
'wget',
'zlib1g-dev',
]
$packages.each | $package | {
if (!defined(Package[$package])) {
package { $package:
ensure => $package_ensure,
}
}
}
class { 'nodejs':
repo_url_suffix => '16.x',
}
class { 'postgresql::server':
}
include redis
exec { 'enable_corepack':
command => '/usr/bin/corepack enable',
creates => '/usr/bin/yarn',
require => Class['nodejs'],
}
exec { 'yarn_classic':
command => '/usr/bin/yarn set version classic',
creates => '/root/.yarnrc',
require => Exec['enable_corepack'],
}
group { $mastodon_group: }
user { $mastodon_user:
gid => $mastodon_group,
home => $mastodon_home,
managehome => true,
system => true,
require => Group[$mastodon_group],
}
vcsrepo { 'rbenv':
path => "${mastodon_home}/.rbenv",
source => 'https://github.com/rbenv/rbenv.git',
provider => 'git',
owner => $mastodon_user,
group => $mastodon_group,
require => User[$mastodon_user],
}
exec { 'configure_rbenv':
command => "${mastodon_home}/.rbenv/src/configure",
user => $mastodon_user,
cwd => "${mastodon_home}/.rbenv/",
creates => "${mastodon_home}/.rbenv/src/Makefile",
require => Vcsrepo['rbenv'],
}
exec { 'make_rbenv':
command => '/usr/bin/make -C src',
user => $mastodon_user,
cwd => "${mastodon_home}/.rbenv/",
creates => "${mastodon_home}/.rbenv/libexec/rbenv",
require => Exec['configure_rbenv'],
}
file_line { 'mastodon_path':
path => "${mastodon_home}/.bashrc",
line => 'export PATH="$HOME/.rbenv/bin:$PATH"',
match => '^export PATH="$HOME/.rbenv',
require => Vcsrepo['rbenv'],
}
file_line { 'mastodon_rbenv_init':
path => "${mastodon_home}/.bashrc",
line => 'eval "$(rbenv init -)"',
match => '^eval "$(rbenv init -)"',
require => Vcsrepo['rbenv'],
}
vcsrepo { 'ruby_build':
path => "${mastodon_home}/.rbenv/plugins/ruby-build",
source => 'https://github.com/rbenv/ruby-build.git',
provider => 'git',
owner => $mastodon_user,
group => $mastodon_group,
require => Vcsrepo['rbenv'],
}
if ($db_password != '') {
postgresql::server::db { $db_name:
user => $db_user,
password => postgresql::postgresql_password($db_user, $db_password),
grant => 'ALL',
}
} else {
postgresql::server::db { $db_name:
user => $db_user,
grant => 'ALL',
}
}
postgresql::server::database_grant { "${db_user}_${db_name}" :
privilege => 'ALL',
db => $db_name,
role => $db_user,
}
vcsrepo { 'mastodon_code':
path => "${mastodon_home}/live",
source => 'https://github.com/mastodon/mastodon.git',
revision => $mastodon_version,
provider => 'git',
owner => $mastodon_user,
group => $mastodon_group,
require => User[$mastodon_user],
}
file { '/usr/local/bin/install_mastodon.sh':
ensure => $ensure,
content => template('mastodon/install_mastodon.sh.erb'),
mode => '0750',
owner => $mastodon_user,
group => 'root',
require => [
Vcsrepo['mastodon_code'],
Postgresql::Server::Db[$db_name],
Vcsrepo['ruby_build'],
],
}
exec { 'install_mastodon':
command => '/usr/local/bin/install_mastodon.sh',
user => $mastodon_user,
group => $mastodon_group,
environment => [
"HOME=${mastodon_home}",
'LANG=C.UTF-8',
"USER=${mastodon_user}",
],
creates => "${mastodon_home}/./.mastodon_install",
path => "${mastodon_home}/.rbenv/shims:${mastodon_home}/.rbenv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
timeout => 0,
require => File['/usr/local/bin/install_mastodon.sh'],
}
$built_config = {
'LOCAL_DOMAIN' => $hostname,
'DB_PASS' => $db_password,
'DB_USER' => $db_user,
'DB_NAME' => $db_name,
'DB_PORT' => $db_port,
'DB_HOST' => $db_host,
'SECRET_KEY_BASE' => $secret_key_base,
'OTP_SECRET' => $otp_secret,
}
$real_config = $config + $built_config
file { "${mastodon_home}/live/.env.production":
ensure => $ensure,
content => template('mastodon/env.production.erb'),
mode => '0640',
owner => $mastodon_user,
group => $mastodon_group,
require => [
Vcsrepo['mastodon_code'],
],
}
# RAILS_ENV=production rails db:setup
exec { 'db_setup':
command => "${mastodon_home}/live/bin/rails db:setup > ${mastodon_home}/db_setup_done",
creates => "${mastodon_home}/db_setup_done",
path => "${mastodon_home}/.rbenv/shims:${mastodon_home}/.rbenv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
environment => [
'RAILS_ENV=production',
],
user => $mastodon_user,
group => $mastodon_group,
cwd => "${mastodon_home}/live",
timeout => 0,
require => File["${mastodon_home}/live/.env.production"],
}
# db:create
# RAILS_ENV=production rails assets:precompile
exec { 'assets_precompile':
command => "${mastodon_home}/live/bin/rails assets:precompile > ${mastodon_home}/assets_precompile_done",
creates => "${mastodon_home}/assets_precompile_done",
path => "${mastodon_home}/.rbenv/shims:${mastodon_home}/.rbenv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
environment => [
'RAILS_ENV=production',
],
user => $mastodon_user,
group => $mastodon_group,
cwd => "${mastodon_home}/live",
timeout => 0,
require => File["${mastodon_home}/live/.env.production"],
}
exec { "register-${hostname}-letsencrypt":
command => "/etc/init.d/apache2 stop && /usr/bin/certbot certonly --agree-tos --email certs@susurrando.com -d ${hostname} -n --standalone && /etc/init.d/apache2 start",
creates => "/etc/letsencrypt/live/${hostname}/privkey.pem",
require => Package['python3-certbot-apache'],
}
apache::vhost { $hostname:
ensure => $ensure,
access_log_file => $hostname,
add_listen => false,
error_log_file => $hostname,
docroot => "${mastodon_home}/live/public",
manage_docroot => false,
proxy_preserve_host => true,
proxy_add_headers => true,
port => 443,
priority => 15,
protocols => [
'h2',
'http/1.1',
],
protocols_honor_order => true,
proxy_requests => false,
proxy_pass => [
{ 'path' => '/500.html', 'url' => '!' },
{ 'path' => '/sw.js', 'url' => '!' },
{ 'path' => '/robots.txt', 'url' => '!' },
{ 'path' => '/manifest.json', 'url' => '!' },
{ 'path' => '/browserconfig.xml', 'url' => '!' },
{ 'path' => '/mask-icon.svg', 'url' => '!' },
],
custom_fragment => '
ServerSignature Off
ProxyPass /api/v1/streaming ws://localhost:4000
ProxyPassReverse /api/v1/streaming ws://localhost:4000
ProxyPass / http://localhost:3000/
ProxyPassReverse / http://localhost:3000/
',
proxy_pass_match => [
{ 'path' => '^(/.*\.(png|ico)$)', 'url' => '!' },
{ 'path' => '^/(assets|avatars|emoji|headers|packs|sounds|system)', 'url' => '!' },
],
request_headers => [
'set X-Forwarded-Proto "https"',
],
headers => [
'always set Strict-Transport-Security "max-age=31536000"',
'always set Strict-Transport-Security "max-age=15552001; includeSubDomains"',
],
directories => [
{
'path' => '^/(assets|avatars|emoji|headers|packs|sounds|system)',
'provider' => 'locationmatch',
'headers' => 'always set Cache-Control "public, max-age=31536000, immutable"',
'deny' => 'from all',
'require' => 'all granted',
},
{
'path' => '/',
'provider' => 'location',
'require' => 'all granted',
},
],
error_documents => [
{ 'error_code' => '500', 'document' => '/500' },
{ 'error_code' => '501', 'document' => '/501' },
{ 'error_code' => '502', 'document' => '/502' },
{ 'error_code' => '503', 'document' => '/503' },
{ 'error_code' => '504', 'document' => '/504' },
],
ssl => true,
ssl_cert => "/etc/letsencrypt/live/${hostname}/fullchain.pem",
ssl_cipher => 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA',
ssl_honorcipherorder => true,
ssl_protocol => 'all -SSLv3 -SSLv2 -TLSv1 -TLSv1.1',
ssl_key => "/etc/letsencrypt/live/${hostname}/privkey.pem",
ssl_proxy_check_peer_cn => 'on',
ssl_proxy_check_peer_expire => 'on',
ssl_proxyengine => true,
ssl_reload_on_change => true,
allow_encoded_slashes => 'on',
require => Exec["register-${hostname}-letsencrypt"],
}
apache::vhost { "${hostname}_insecure":
ensure => $ensure,
servername => $hostname,
access_log_file => "${hostname}_insecure",
error_log_file => "${hostname}_insecure",
add_listen => false,
ip => '0.0.0.0',
port => 80,
docroot => "${mastodon_home}/live/public",
redirect_status => 'permanent',
redirect_dest => "https://${hostname}/",
custom_fragment => 'ServerSignature Off',
}
systemd::unit_file { 'mastodon-sidekiq.service':
ensure => present,
content => template('mastodon/mastodon-sidekiq.service.erb'),
active => true,
enable => true,
}
systemd::unit_file { 'mastodon-streaming.service':
ensure => present,
content => template('mastodon/mastodon-streaming.service.erb'),
active => true,
enable => true,
}
systemd::unit_file { 'mastodon-streaming@.service':
ensure => present,
content => template('mastodon/mastodon-streaming@.service.erb'),
# active => true,
enable => true,
}
systemd::unit_file { 'mastodon-web.service':
ensure => present,
content => template('mastodon/mastodon-web.service.erb'),
active => true,
enable => true,
}
$users.each | $user | {
if ($user['confirmed']) {
$confirmed = '--confirmed'
} else {
$confirmed = ''
}
exec { "create_user_${user['username']}":
command => "${mastodon_home}/live/bin/tootctl accounts create '${user['username']}' --email '${user['email']}' ${confirmed} --role '${user['role']}' > '${mastodon_home}/create_user_${user['username']}'",
creates => "${mastodon_home}/create_user_${user['username']}",
path => "${mastodon_home}/.rbenv/shims:${mastodon_home}/.rbenv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
environment => [
'RAILS_ENV=production',
],
user => $mastodon_user,
group => $mastodon_group,
cwd => "${mastodon_home}/live",
timeout => 0,
require => File["${mastodon_home}/live/.env.production"],
}
}
#
# Maintenance tasks
# tootctl maintenance fix-duplicates
# tootctl media remove --days 180
# tootctl media remove-orphans
# tootctl preview_cards remove --days 180
}