Configuration
Tomo is configured via a .tomo/config.rb
file. This configuration file defines what tasks to run when executing a setup or deploy, the settings that affect the behavior of those tasks, and the remote host or hosts where those tasks will be run.
The format of tomo’s configuration file is designed to be simple and concise for basic deployments, with the flexibility to scale to more advanced setups that involve multiple roles, environments, and hosts.
A basic deployment will typically use these configuration directives:
Here’s an abbreviated example:
plugin "git"
plugin "bundler"
plugin "rails"
# ...
host "deployer@app.example.com"
set application: "my-rails-app"
set deploy_to: "/var/www/%{application}"
set git_url: "git@github.com:my-username/my-rails-app.git"
set git_branch: "main"
# ...
setup do
run "git:clone"
run "git:create_release"
run "bundler:install"
run "rails:db_schema_load"
# ...
end
deploy do
run "git:create_release"
run "core:symlink_shared"
run "core:write_release_json"
run "bundler:install"
run "rails:assets_precompile"
run "rails:db_migrate"
run "core:symlink_current"
# ...
end
A more complex deployment may make use of these additional directives:
plugin(name_or_relative_path)
Load a tomo plugin by name or from a Ruby file by a relative path.
Several plugins are built into tomo: bundler, env, git, nodenv, puma, rails, and rbenv. If you want to use the tasks provided by one of these plugins, load it by name, like this:
plugin "git"
Plugins can also be provided by gems installed on your system. For example, the tomo-plugin-sidekiq
gem provides the “sidekiq” plugin. Make sure the gem is installed (e.g. in your Gemfile) and then reference the plugin by name to load it:
plugin "sidekiq"
Note that the name of the plugin may not necessarily match the name of the gem. Refer to the gem’s documentation for installation instructions.
Finally, if the argument to plugin
starts with a dot (.
) it is considered a relative path to a custom plugin. By convention, custom plugins are stored in .tomo/plugins/
within the project that tomo is deploying. The name of the plugin is inferred from its file name. So for example, if the plugin is loaded from a file named foo.rb
, then the name of the plugin is “foo” and all tasks it defines will be given the foo:
namespace:
plugin "./plugins/foo.rb"
host(address, **options)
Specify the SSH host address (including username) that tomo will connect to. For example:
host "deployer@app.example.com" # port 22 is implied
host "admin@192.168.1.50", port: 8022 # port 8022
The following advanced options
are supported:
Name | Purpose | Default |
---|---|---|
port |
SSH port number. | 22 |
roles |
An array of String roles to assign to this host. Used with the role directive for specifying which tasks should run on this host. | [] |
log_prefix |
A String prefix to print next to all log output for this host. | nil |
privileged_user |
The SSH user to connect as when running privileged tasks. See setup for an example. | "root" |
set(hash)
Specify a value for a tomo setting. For example, to change the number of releases that tomo retains when pruning old releases:
set keep_releases: 5
For a full list of settings that affect tomo’s core behavior, refer to the core plugin documentation. Each plugin such as bundler and git also has its own specialized list of settings. Refer to the each plugin’s documentation for a full reference.
Interpolation
It is possible to reference other settings when specifying a value. The format of a reference string is %{name}
where name
is the name of another setting. This is often used to build paths that are relative to the release that is being deployed, or for paths relative to tomo’s shared directory.
In this example, the value will be interpolated to contain the release that is being deployed:
set release_json_path: "%{release_path}/.tomo_release.json"
# => "/var/www/my-app/20190523234156/.tomo_release.json"
Another common use case is the shared directory:
set bundler_path: "%{shared_path}/bundle"
# => "/var/www/my-app/shared/bundle"
Interpolation takes place after tomo has loaded all configuration, plugins, and overrides, just before tasks are run.
Custom settings
set
will define a setting if it does not already exist. This means you can create arbitrarily-named settings for your own purposes, such as for use within custom tasks.
set my_setting_i_just_made_up: "great"
In practice most settings are strings, but any Ruby value is possible.
set some_double: 0.57
set my_hash: { urgent: true, message: "hello" }
Overrides
Settings defined by set
can be overridden when running a tomo command, e.g. tomo deploy
, by way of environment variables and command-line arguments.
Environment variable overrides take the form of TOMO_*
. For example, this will override the :git_branch
setting to be “develop”:
$ export TOMO_GIT_BRANCH=develop
$ tomo deploy
On the command line, -s
or --setting
can be used. For example:
$ tomo deploy -s git_branch=develop
The precedence of overrides is as follows (higher in the list have higher precedence):
- Command-line overrides
- Environment variable overrides
set
- Defaults (specified by plugins)
setup(&block)
Define the list of tasks that will be run by the tomo setup
command, by providing a block containing run
directives, like this:
setup do
run "env:setup"
run "core:setup_directories"
run "git:config"
run "git:clone"
run "git:create_release"
run "core:symlink_shared"
run "nodenv:install"
run "rbenv:install"
run "bundler:upgrade_bundler"
run "bundler:config"
run "bundler:install"
run "rails:db_create"
run "rails:db_schema_load"
run "rails:db_seed"
run "puma:setup_systemd"
end
Each run
can optionally take a privileged: true
option. When specified, the task will be run using the “root” user instead of the default user specified for each host
.
setup do
run "apt:install", privileged: true
end
deploy(&block)
Define the list of tasks that will be run by the tomo deploy
command, by providing a block containing run
directives, like this:
deploy do
run "env:update"
run "git:create_release"
run "core:symlink_shared"
run "core:write_release_json"
run "bundler:install"
run "rails:db_migrate"
run "rails:db_seed"
run "rails:assets_precompile"
run "core:symlink_current"
run "puma:restart"
run "puma:check_active"
run "core:clean_releases"
run "bundler:clean"
run "core:log_revision"
end
environment(name, &block)
Define an environment so that tomo can be used to deploy the same project with more than one set of configuration. Each environment must have a unique name and can contain its own host and set directives. For example:
# Top-level config is shared by both environments
set git_url: "git@github.com:username/repo.git"
environment :staging do
host "deployer@staging.example.com"
set git_branch: "develop"
end
environment :production do
host "deployer@app.example.com"
set git_branch: "main"
end
Use the -e
or --environment
option when running tomo to select which environment to use.
role(name, runs:)
Specify that certain task(s) are only allowed to run on hosts that have the role name
. The runs
option must be an array of Strings representing task names. Simple wildcards (glob rules using *
) can be used to match multiple tasks.
By default, every task that is listed in setup and deploy blocks is run on every host. In a multi-host deployment this is not always desirable. For example, the rails:db_seed
and rails:db_migrate
tasks should only be run once per deployment (i.e. on one host). To accomplish this, we can define a role named “db” that is responsible for running these tasks, like this:
role "db", runs: ["rails:db_*"]
host "deployer@app1.example.com", roles: ["db"]
host "deployer@app2.example.com", roles: []
The role
directive in the example above tells tomo that any task matching the glob pattern rails:db_*
should only run on hosts that are assigned the “db” role. That means that app1.example.com will run rails:db_seed
and rails:db_migrate
, but app2.example.com will not.
batch(&block)
Define a group tasks to run in parallel during a multi-host deploy. This allows one host to “race ahead” of other hosts and leads to potentially faster deployments.
In a multi-host deployment, by default each task in a setup and deploy must complete on all hosts before tomo will move onto the next task. This means a deployment is limited by its slowest host. If a task is configured via role to run on only one host (e.g. rails:db_migrate
), other hosts must wait until the task is done.
We can speed this up by using batch
, as in this example:
deploy do
# All tasks in this batch must complete before tomo will move onto
# core:symlink_current, but within the batch each host can "race ahead"
# independently in parallel.
batch do
run "env:update"
run "git:create_release"
run "core:symlink_shared"
run "core:write_release_json"
run "bundler:install"
run "rails:assets_precompile"
run "rails:db_migrate"
end
# This task must complete on all hosts before moving onto the next batch.
run "core:symlink_current"
# The tasks within this batch can run independently in parallel on each host.
batch do
run "puma:restart"
run "core:clean_releases"
run "bundler:clean"
run "core:log_revision"
end
end
At runtime, tomo turns this configuration into an “execution plan”, which you can see by passing the --debug
option to tomo deploy
. Here’s what the execution plan might look like for the above configuration with two hosts:
DEBUG: Execution plan:
CONCURRENTLY (2 THREADS):
= CONNECT deployer@app1.example.com
= CONNECT deployer@app2.example.com
CONCURRENTLY (2 THREADS):
= IN SEQUENCE:
RUN env:update ON deployer@app1.example.com
RUN git:create_release ON deployer@app1.example.com
RUN core:symlink_shared ON deployer@app1.example.com
RUN core:write_release_json ON deployer@app1.example.com
RUN bundler:install ON deployer@app1.example.com
RUN rails:assets_precompile ON deployer@app1.example.com
RUN rails:db_migrate ON deployer@app1.example.com
= IN SEQUENCE:
RUN env:update ON deployer@app2.example.com
RUN git:create_release ON deployer@app2.example.com
RUN core:symlink_shared ON deployer@app2.example.com
RUN core:write_release_json ON deployer@app2.example.com
RUN bundler:install ON deployer@app2.example.com
RUN rails:assets_precompile ON deployer@app2.example.com
CONCURRENTLY (2 THREADS):
= RUN core:symlink_current ON deployer@app1.example.com
= RUN core:symlink_current ON deployer@app2.example.com
CONCURRENTLY (2 THREADS):
= IN SEQUENCE:
RUN puma:restart ON deployer@app1.example.com
RUN core:clean_releases ON deployer@app1.example.com
RUN bundler:clean ON deployer@app1.example.com
RUN core:log_revision ON deployer@app1.example.com
= IN SEQUENCE:
RUN puma:restart ON deployer@app2.example.com
RUN core:clean_releases ON deployer@app2.example.com
RUN bundler:clean ON deployer@app2.example.com
RUN core:log_revision ON deployer@app2.example.com
As we can see, core:symlink_current
is executed at the same time on both hosts, but before and after that, the other tasks can be executed out of sync.