rails: Support background workers and other Rails services/workers

The new `services' option is used to request additional processes be
run in the background with the same environment as the main Rails
process.
This commit is contained in:
Peter Jones 2019-01-04 11:17:40 -07:00
parent 193b82189e
commit 4964d95974
No known key found for this signature in database
GPG key ID: 9DAFAA8D01941E49
6 changed files with 130 additions and 23 deletions

View file

@ -1,5 +1,5 @@
# Functions for working with NixOps keys.
{ lib }:
{ lib, ... }:
with lib;

27
lib/shell.nix Normal file
View file

@ -0,0 +1,27 @@
# Functions for generating and working with shell scripts:
{ lib, pkgs, ... }:
with lib;
let
funcs = rec {
# Generate a shell script that exports the given variables.
#
# Type:
#
# string -> attrset -> derivation
#
# Arguments:
#
# fileName: The name of the file in the nix store to create.
# attrs: The variables to include in the generated script.
#
attrsToShellExports = fileName: attrs:
let export = name: value: "export ${name}=${escapeShellArg value}";
lines = mapAttrsToList export attrs;
in pkgs.writeText fileName (concatStringsSep "\n" lines);
};
in funcs

View file

@ -5,9 +5,10 @@ with lib;
let
libFiles = [
../lib/keys.nix
../lib/shell.nix
];
loadLib = path: import path { inherit lib; };
loadLib = path: import path { inherit lib pkgs; };
libs = foldr (a: b: recursiveUpdate (loadLib a) b) {} libFiles;
in

View file

@ -12,6 +12,17 @@ let
scripts = import ./scripts.nix { inherit lib pkgs; };
options = import ./options.nix { inherit config lib pkgs; };
##############################################################################
# The main Rails service:
mainService = app: {
name = "main";
isMain = true;
script = ''
puma -e ${app.railsEnv} -p ${toString app.port}
'';
};
##############################################################################
# Is PostgreSQL local?
localpg = config.phoebe.services.postgresql.enable;
@ -21,6 +32,18 @@ let
# 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:
collectApps = f: foldr (a: b: recursiveUpdate b (f a)) {} (attrValues cfg.apps);
@ -55,24 +78,15 @@ let
##############################################################################
# Generate a systemd service for a Ruby on Rails application:
appService = app: {
"rails-${app.name}" = {
description = "${app.name} (Ruby on Rails)";
appService = app: service: {
"rails-${app.name}-${service.name}" = {
description = "${app.name} (Ruby on Rails) ${service.name}";
path = appPath app;
environment = {
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;
environment = appEnv app;
wantedBy = [ "multi-user.target" ];
wants =
optional (!service.isMain) "rails-${app.name}-main" ++
plib.keyService app.database.passwordFile ++
plib.keyService app.sourcedFile;
@ -80,10 +94,11 @@ let
[ "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 = ''
preStart = optionalString service.isMain ''
# Prepare the config directory:
rm -rf ${app.home}/config
mkdir -p ${app.home}/{config,log,tmp,db,state}
@ -93,8 +108,12 @@ let
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) ''
@ -106,9 +125,9 @@ let
chmod go+rx $(dirname "${app.home}")
chmod u+w ${app.home}/db/schema.rb
'' + optionalString app.database.migrate ''
# Migrate the database (use sudo so environment variables go through):
${pkgs.sudo}/bin/sudo -u rails-${app.name} -EH \
'' + 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
@ -116,7 +135,7 @@ let
script = ''
${optionalString (app.sourcedFile != null) ". ${app.home}/state/sourcedFile.sh"}
${app.package.rubyEnv}/bin/puma -e ${app.railsEnv} -p ${toString app.port}
${service.script}
'';
serviceConfig = {
@ -132,6 +151,13 @@ let
};
};
##############################################################################
# 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 );
##############################################################################
# Generate a user account for a Ruby on Rails application:
appUser = app: {
@ -173,7 +199,8 @@ in
# Each application gets a user account:
users = collectApps appUser;
# Each application gets a systemd service to keep it running.
systemd.services = collectApps appService;
# Each application gets one or more systemd services to keep it
# running.
systemd.services = collectApps appServices;
};
}

View file

@ -52,6 +52,35 @@ let
};
};
##############################################################################
# Service configuration:
service = { name, ... }: {
options = {
name = mkOption {
type = types.str;
example = "sidekiq";
description = "The name of the additional service to run.";
};
script = mkOption {
type = types.lines;
example = "sidekiq -c 5 -v -q default";
description = "Shell commands executed as the service's main process.";
};
isMain = mkOption {
internal = true;
type = types.bool;
default = false;
description = "Is this the main Rails process?";
};
};
config = {
name = mkDefault name;
};
};
##############################################################################
# Application configuration:
application = { name, ... }: {
@ -88,6 +117,20 @@ let
description = "Database configuration.";
};
services = mkOption {
type = types.attrsOf (types.submodule service);
default = { };
description = ''
Additional services to run for this Rails application. For
example, if you need to have background queue processing
scripts running this is where you'd want to do that.
All of the listed services are run via systemd and are
executed in the same environment as the main Rails
application itself.
'';
};
railsEnv = mkOption {
type = types.str;
default = "production";

View file

@ -0,0 +1,9 @@
# Rails user shell profile.
if [ -e "$HOME/.env" ]; then
. "$HOME/.env"
fi
if [ -e "$HOME/../state/sourcedFile.sh" ]; then
. "$HOME/../state/sourcedFile.sh"
fi