Browse Source

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)
pjones/monitoring
Peter J. Jones 9 months ago
parent
commit
e742614c30
Signed by: Peter Jones <pjones@devalot.com> GPG Key ID: 9DAFAA8D01941E49

+ 6
- 138
modules/services/web/rails/default.nix View File

@@ -8,42 +8,11 @@ let
##############################################################################
# Save some typing.
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:
mainService = app: {
name = "main";
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;
options = import ./options.nix { inherit config lib pkgs; };
appSystemd = import ./systemd.nix { inherit config pkgs lib; };
funcs = import ./functions.nix { inherit config; };

##############################################################################
# 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:
appUser = app: {
@@ -202,7 +71,7 @@ let
group = "rails-${app.name}";
shell = "${pkgs.bash}/bin/bash";
extraGroups = [ config.services.nginx.group ];
packages = appPath app;
packages = funcs.appPath app;
};
groups."rails-${app.name}" = {};
};
@@ -234,9 +103,8 @@ in
users = collectApps appUser;

# Each application gets one or more systemd services to keep it
# running.
systemd.services = collectApps appServices;
systemd.timers = collectApps appTimers;
# running. There's also a systemd target and some timers.
systemd = collectApps appSystemd;

# Rotate all of the log files:
services.logrotate = {

+ 27
- 0
modules/services/web/rails/functions.nix View File

@@ -1,7 +1,34 @@
{ config }:

rec {

##############################################################################
# The default base directory for Rails applications:
base = "/var/lib/rails";

##############################################################################
# Where a Rails application lives:
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;

}

+ 15
- 1
modules/services/web/rails/options.nix View File

@@ -4,7 +4,7 @@ with lib;

let
##############################################################################
functions = import ./functions.nix;
functions = import ./functions.nix { inherit config; };

##############################################################################
# Database configuration:
@@ -102,6 +102,20 @@ let
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 {
type = types.path;
description = "The directory where the application is deployed to.";

+ 141
- 0
modules/services/web/rails/systemd.nix 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

Loading…
Cancel
Save