rails: New systemd target for each Rails application

* Each now has a systemd target so you can start and stop all
    services together.

  * New `enable' option so you can prevent services from
    starting (e.g., on development machines)
This commit is contained in:
Peter Jones 2019-01-24 12:16:19 -07:00
parent eaa5c89f6b
commit e742614c30
No known key found for this signature in database
GPG key ID: 9DAFAA8D01941E49
4 changed files with 189 additions and 139 deletions

View file

@ -8,42 +8,11 @@ let
############################################################################## ##############################################################################
# Save some typing. # Save some typing.
cfg = config.phoebe.services.rails; cfg = config.phoebe.services.rails;
plib = config.phoebe.lib;
scripts = import ./scripts.nix { inherit lib pkgs; };
options = import ./options.nix { inherit config lib pkgs; };
############################################################################## ##############################################################################
# The main Rails service: options = import ./options.nix { inherit config lib pkgs; };
mainService = app: { appSystemd = import ./systemd.nix { inherit config pkgs lib; };
name = "main"; funcs = import ./functions.nix { inherit config; };
schedule = null;
isMain = true;
script = ''
puma -e ${app.railsEnv} -p ${toString app.port}
'';
};
##############################################################################
# Is PostgreSQL local?
localpg = config.phoebe.services.postgresql.enable;
##############################################################################
# Packages to put in the application's PATH. FIXME:
# propagatedBuildInputs won't always be set.
appPath = app: [ app.package.rubyEnv ] ++ app.package.propagatedBuildInputs;
##############################################################################
# All of the environment variables that a Rails app needs:
appEnv = app: {
HOME = "${app.home}/home";
RAILS_ENV = app.railsEnv;
DATABASE_HOST = app.database.host;
DATABASE_PORT = toString app.database.port;
DATABASE_NAME = app.database.name;
DATABASE_USER = app.database.user;
DATABASE_PASSWORD_FILE = "${app.home}/state/database.password";
} // app.environment;
############################################################################## ##############################################################################
# Collect all apps into a single set using the given function: # Collect all apps into a single set using the given function:
@ -92,106 +61,6 @@ let
} }
''; '';
##############################################################################
# Generate a systemd service for a Ruby on Rails application:
appService = app: service: {
"rails-${app.name}-${service.name}" = {
description = "${app.name} (Ruby on Rails) ${service.name}";
path = appPath app;
environment = appEnv app;
# Only start this service if it isn't scheduled by a timer.
wantedBy = optional (service.schedule == null) "multi-user.target";
wants =
plib.keyService app.database.passwordFile ++
plib.keyService app.sourcedFile;
after =
[ "network.target" ] ++
optional localpg "postgresql.service" ++
optional localpg "pg-accounts.service" ++
optional (!service.isMain) "rails-${app.name}-main" ++
plib.keyService app.database.passwordFile ++
plib.keyService app.sourcedFile;
preStart = optionalString service.isMain ''
# Prepare the config directory:
rm -rf ${app.home}/config
mkdir -p ${app.home}/{config,log,tmp,db,state}
cp -rf ${app.package}/share/${app.name}/config.dist/* ${app.home}/config/
cp ${app.package}/share/${app.name}/db/schema.rb.dist ${app.home}/db/schema.rb
cp ${./database.yml} ${app.home}/config/database.yml
cp ${app.database.passwordFile} ${app.home}/state/database.password
# Additional set up for the home directory:
mkdir -p ${app.home}/home
ln -nfs ${app.package}/share/${app.name} ${app.home}/home/${app.name}
ln -nfs ${plib.attrsToShellExports "rails-${app.name}-env" (appEnv app)} ${app.home}/home/.env
cp ${./profile.sh} ${app.home}/home/.profile
chmod 0700 ${app.home}/home/.profile
# Copy the sourcedFile if necessary:
${optionalString (app.sourcedFile != null) ''
cp ${app.sourcedFile} ${app.home}/state/sourcedFile.sh
''}
# Fix permissions:
chown -R rails-${app.name}:rails-${app.name} ${app.home}
chmod go+rx $(dirname "${app.home}")
chmod u+w ${app.home}/db/schema.rb
'' + optionalString (service.isMain && app.database.migrate) ''
# Migrate the database:
${pkgs.sudo}/bin/sudo --user=rails-${app.name} --login \
${scripts}/bin/db-migrate.sh \
-r ${app.package}/share/${app.name} \
-s ${app.home}/state
'';
script = ''
${optionalString (app.sourcedFile != null) ". ${app.home}/state/sourcedFile.sh"}
${service.script}
'';
serviceConfig = {
WorkingDirectory = "${app.package}/share/${app.name}";
Restart = "on-failure";
TimeoutSec = "infinity"; # FIXME: what's a reasonable amount of time?
Type = "simple";
PermissionsStartOnly = true;
User = "rails-${app.name}";
Group = "rails-${app.name}";
UMask = "0077";
};
};
};
##############################################################################
# Schedule some services with a systemd timer:
appTimer = app: service: optionalAttrs (service.schedule != null) {
"rails-${app.name}-${service.name}" = {
description = "${app.name} (Ruby on Rails) ${service.name}";
wantedBy = [ "timers.target" ];
timerConfig.OnCalendar = service.schedule;
timerConfig.Unit = "rails-${app.name}-${service.name}.service";
};
};
##############################################################################
# Collect all services for a given application and turn them into
# systemd services.
appServices = app:
foldr (service: set: recursiveUpdate set (appService app service)) {}
( [(mainService app)] ++ attrValues app.services );
##############################################################################
# Collect all services and turn them into systemd timers:
appTimers = app:
foldr (service: set: recursiveUpdate set (appTimer app service)) {}
(attrValues app.services);
############################################################################## ##############################################################################
# Generate a user account for a Ruby on Rails application: # Generate a user account for a Ruby on Rails application:
appUser = app: { appUser = app: {
@ -202,7 +71,7 @@ let
group = "rails-${app.name}"; group = "rails-${app.name}";
shell = "${pkgs.bash}/bin/bash"; shell = "${pkgs.bash}/bin/bash";
extraGroups = [ config.services.nginx.group ]; extraGroups = [ config.services.nginx.group ];
packages = appPath app; packages = funcs.appPath app;
}; };
groups."rails-${app.name}" = {}; groups."rails-${app.name}" = {};
}; };
@ -234,9 +103,8 @@ in
users = collectApps appUser; users = collectApps appUser;
# Each application gets one or more systemd services to keep it # Each application gets one or more systemd services to keep it
# running. # running. There's also a systemd target and some timers.
systemd.services = collectApps appServices; systemd = collectApps appSystemd;
systemd.timers = collectApps appTimers;
# Rotate all of the log files: # Rotate all of the log files:
services.logrotate = { services.logrotate = {

View file

@ -1,7 +1,34 @@
{ config }:
rec { rec {
##############################################################################
# The default base directory for Rails applications: # The default base directory for Rails applications:
base = "/var/lib/rails"; base = "/var/lib/rails";
##############################################################################
# Where a Rails application lives: # Where a Rails application lives:
home = name: "${base}/${name}"; home = name: "${base}/${name}";
##############################################################################
# Is PostgreSQL local?
localpg = config.phoebe.services.postgresql.enable;
##############################################################################
# Packages to put in the application's PATH. FIXME:
# propagatedBuildInputs won't always be set.
appPath = app: [ app.package.rubyEnv ] ++ app.package.propagatedBuildInputs;
##############################################################################
# All of the environment variables that a Rails app needs:
appEnv = app: {
HOME = "${app.home}/home";
RAILS_ENV = app.railsEnv;
DATABASE_HOST = app.database.host;
DATABASE_PORT = toString app.database.port;
DATABASE_NAME = app.database.name;
DATABASE_USER = app.database.user;
DATABASE_PASSWORD_FILE = "${app.home}/state/database.password";
} // app.environment;
} }

View file

@ -4,7 +4,7 @@ with lib;
let let
############################################################################## ##############################################################################
functions = import ./functions.nix; functions = import ./functions.nix { inherit config; };
############################################################################## ##############################################################################
# Database configuration: # Database configuration:
@ -102,6 +102,20 @@ let
description = "The name of the Ruby on Rails application."; description = "The name of the Ruby on Rails application.";
}; };
enable = mkOption {
type = types.bool;
default = true;
example = false;
description = ''
Whether to enable this application by default.
Setting this value to false will prevent any of the systemd
services from starting. This is useful for creating
development environments where everything is set up but
nothing is running.
'';
};
home = mkOption { home = mkOption {
type = types.path; type = types.path;
description = "The directory where the application is deployed to."; description = "The directory where the application is deployed to.";

View file

@ -0,0 +1,141 @@
{ config
, pkgs
, lib
}:
with lib;
let
##############################################################################
# Helpful functions.
plib = config.phoebe.lib;
funcs = import ./functions.nix { inherit config; };
scripts = import ./scripts.nix { inherit lib pkgs; };
##############################################################################
# The main Rails service:
mainService = app: {
name = "main";
schedule = null;
isMain = true;
script = ''
puma -e ${app.railsEnv} -p ${toString app.port}
'';
};
##############################################################################
# Generate a systemd service for a Ruby on Rails application:
appService = app: service: {
"rails-${app.name}-${service.name}" = {
description = "${app.name} (Ruby on Rails) ${service.name}";
path = funcs.appPath app;
environment = funcs.appEnv app;
# Only start this service if it isn't scheduled by a timer.
partOf = optional (service.schedule == null) "rails-${app.name}.target";
wantedBy = optional (service.schedule == null) "rails-${app.name}.target";
wants =
plib.keyService app.database.passwordFile ++
plib.keyService app.sourcedFile;
after =
[ "network.target" ] ++
optional funcs.localpg "postgresql.service" ++
optional funcs.localpg "pg-accounts.service" ++
optional (!service.isMain) "rails-${app.name}-main" ++
plib.keyService app.database.passwordFile ++
plib.keyService app.sourcedFile;
preStart = optionalString service.isMain ''
# Prepare the config directory:
rm -rf ${app.home}/config
mkdir -p ${app.home}/{config,log,tmp,db,state}
cp -rf ${app.package}/share/${app.name}/config.dist/* ${app.home}/config/
cp ${app.package}/share/${app.name}/db/schema.rb.dist ${app.home}/db/schema.rb
cp ${./database.yml} ${app.home}/config/database.yml
cp ${app.database.passwordFile} ${app.home}/state/database.password
# Additional set up for the home directory:
mkdir -p ${app.home}/home
ln -nfs ${app.package}/share/${app.name} ${app.home}/home/${app.name}
ln -nfs ${plib.attrsToShellExports "rails-${app.name}-env" (funcs.appEnv app)} ${app.home}/home/.env
cp ${./profile.sh} ${app.home}/home/.profile
chmod 0700 ${app.home}/home/.profile
# Copy the sourcedFile if necessary:
${optionalString (app.sourcedFile != null) ''
cp ${app.sourcedFile} ${app.home}/state/sourcedFile.sh
''}
# Fix permissions:
chown -R rails-${app.name}:rails-${app.name} ${app.home}
chmod go+rx $(dirname "${app.home}")
chmod u+w ${app.home}/db/schema.rb
'' + optionalString (service.isMain && app.database.migrate) ''
# Migrate the database:
${pkgs.sudo}/bin/sudo --user=rails-${app.name} --login \
${scripts}/bin/db-migrate.sh \
-r ${app.package}/share/${app.name} \
-s ${app.home}/state
'';
script = ''
${optionalString (app.sourcedFile != null) ". ${app.home}/state/sourcedFile.sh"}
${service.script}
'';
serviceConfig = {
WorkingDirectory = "${app.package}/share/${app.name}";
Restart = "on-failure";
TimeoutSec = "infinity"; # FIXME: what's a reasonable amount of time?
Type = "simple";
PermissionsStartOnly = true;
User = "rails-${app.name}";
Group = "rails-${app.name}";
UMask = "0077";
};
};
};
##############################################################################
# Schedule some services with a systemd timer:
appTimer = app: service: optionalAttrs (service.schedule != null) {
"rails-${app.name}-${service.name}" = {
description = "${app.name} (Ruby on Rails) ${service.name}";
wantedBy = optional app.enable "timers.target";
timerConfig.OnCalendar = service.schedule;
timerConfig.Unit = "rails-${app.name}-${service.name}.service";
};
};
##############################################################################
# Collect all services for a given application and turn them into
# systemd services.
appServices = app:
foldr (service: set: recursiveUpdate set (appService app service)) {}
( [(mainService app)] ++ attrValues app.services );
##############################################################################
# Collect all services and turn them into systemd timers:
appTimers = app:
foldr (service: set: recursiveUpdate set (appTimer app service)) {}
(attrValues app.services);
##############################################################################
# All systemd settings for an application:
appSystemd = app: {
targets."rails-${app.name}" = {
description = "${app.name} (Ruby on Rails)";
wantedBy = optional app.enable "multi-user.target";
};
services = appServices app;
timers = appTimers app;
};
in appSystemd