mix_deploy
mix_deploy copied to clipboard
Library of mix tasks to deploy an Elixir release to servers (cloud instance or bare metal, automated deploy)
mix_deploy
This module generates scripts which help deploy an Erlang release, handling tasks such as creating initial directory structure, unpacking release files, managing configuration, and starting/stopping. It supports deployment to the local machine, bare-metal servers or cloud servers using e.g. AWS CodeDeploy.
It supports releases created with Elixir 1.9+ mix release or Distillery.
It assumes that mix_systemd is used to generate a systemd unit file for the application, and shares conventions with it about naming files. See mix_systemd for examples.
Here is a complete example app.
Installation
Add mix_deploy to the list of dependencies in mix.exs:
def deps do
[
{:mix_deploy, "~> 0.7"},
]
end
Example
The most straightforward way to deploy an app is on a server at e.g. Digital Ocean. You can build and deploy on the same machine, checking out the code on a server, building/testing, then generating a release. You then run scripts to set up the runtime environment, including systemd unit scripts, extract the release to the target dir and run it under systemd.
Configure the app
Follow the Phoenix config process for
deployment and
releases. Make the app read its
config from environment variables. Create a file with these environment vars
and put it in config/environment.
Configure mix_deploy and mix_systemd in config/config.exs
config :mix_systemd,
env_files: [
# Read environment vars from file /srv/foo/etc/environment
["-", :deploy_dir, "/etc/environment"],
],
# Set individual env vars
env_vars: [
"PORT=8080"
],
# Run app under this OS user, default is name of app
app_user: "app",
app_group: "app"
config :mix_deploy,
app_user: "app",
app_group: "app"
# When deploying, copy config/environment to /etc/foo/environment
copy_files: [
%{
src: "config/environment",
dst: [:deploy_dir, "/etc/environment"],
user: "$DEPLOY_USER",
group: "$APP_GROUP",
mode: "640"
},
],
create_dirs: [
%{
path: [:deploy_dir, "/etc"],
user: "$DEPLOY_USER",
group: "$APP_GROUP",
mode: "750"
},
],
# Generate these scripts in bin
templates: [
"init-local",
"create-users",
"create-dirs",
"copy-files",
"enable",
"release",
"restart",
"rollback",
"start",
"stop",
]
Initialize mix_systemd and mix_deploy and generate files.
# Initialize mix_systemd templates
mix systemd.init
# Initialize mix_deploy templates
mix deploy.init
# Create systemd unit file for app under _build/prod/systemd
MIX_ENV=prod mix systemd.generate
# Create deploy scripts project `bin` dir
MIX_ENV=prod mix deploy.generate
chmod +x bin/*
Set up the system.
This creates the app OS user, directory structure under /srv/foo, and the
systemd unit file which supervises the app.
deploy-init-local is a convenience script which runs other scripts to set up
the system:
sudo bin/deploy-init-local
It does the following:
# Create users to run the app
bin/deploy-create-users
# Create deploy dirs under /srv/foo
bin/deploy-create-dirs
# Copy scripts used at runtime by the systemd unit
# Strictly speaking, you only need to copy scripts used at runtime
cp bin/* /srv/foo/bin
# Copy files and enable systemd unit
bin/deploy-copy-files
bin/deploy-enable
Build the Elixir release
Create the release, a tar file containing the app, the libraries it depends on, and scripts to manage it.
MIX_ENV=prod mix release
Deploy the release to the local machine:
# Extract release to target directory, creating current symlink
bin/deploy-release
# Restart the systemd unit
sudo bin/deploy-restart
Roll back the release with the following:
bin/deploy-rollback
sudo bin/deploy-restart
Try it out
Your app should now be running:
curl -v http://localhost:8080/
If it is not, have a look at the logs.
systemctl status foo
journalctl -u foo
If you want it to run on port 80, you can redirect 80 to 8080 in the firewall.
Usage
First, use the deploy.init task to template files from the library to the
rel/templates/deploy directory in your project.
mix deploy.init
Next, generate the scripts based on your project's config:
MIX_ENV=prod mix deploy.generate
chmod +x bin/*
By default, mix deploy.generate creates scripts under a bin directory at
the top level of your project. If you want to keep them separate, e.g. to
create different files based on the environment, set bin_dir to
[:output_dir, "bin"] and it will generate files under e.g. _build/prod/deploy.
Configuration
The library tries to choose smart defaults. It reads the app name from
mix.exs and calculates default values for its configuration parameters.
If your app is named foo_bar, it will create a service named foo-bar,
deployed to /srv/foo-bar, running under the user foo-bar.
The library doesn't generate any output scripts by default, you need to enable
them with the templates parameter. It can create the following scripts:
Systemd scripts
These are wrappers on e.g. /bin/systemctl restart foo. They are useful for
e.g. CodeDeploy hook scripts where we have to run a script without parameters.
deploy-start: Start servicesdeploy-stop: Stop servicesdeploy-restart: Restart servicesdeploy-enable: Enable systemd units
System setup scripts
These scripts set up the target system for the application. They are useful for local and automated deploy.
deploy-create-users: Create OS accounts for app and deploy usersdeploy-create-dirs: Create dirs, including the release dir/srv/fooand standard dirs like/etc/fooif needed.
Local deploy scripts
These scripts deploy the app to the same server as it was built on:
deploy-copy-files: Copy files from_buildto target/srv/foo, or to a staging directory for packagingdeploy-release: Deploy release, extracting to a timestamped dir under/srv/foo/releases, then making a symlink from/srv/foo/currentdeploy-rollback: Rollback release, resetting the symlink to point to the previous release
The library also has mix tasks to deploy and roll back releases:
mix deploy.local
mix deploy.local.rollback
CodeDeploy deploy scripts
These scripts run on the target machine as lifecycle hooks.
deploy-clean-target: Delete files under target dir in preparation for deploying updatedeploy-extract-release: Extract release from tardeploy-set-perms: Set target file permissions so that they can be used by the app user
Build server scripts
These scripts run on the build server.
deploy-stage-files: Copy output files to staging directory, defaultfiles
Release command scripts
These scripts set up the environment and then run release commands.
They make the config match the environment vars set at runtime in the systemd
unit. With Elixir 1.9+ you can source /srv/foo/bin/set-env in rel/env.sh.eex.
The other scripts are mainly useful with Distillery.
set-env: Set up environmentdeploy-migrate: Migrate database on target system by running a custom command.deploy-remote-console: Launch remote console for the app
Runtime environment scripts
These scripts are called by the systemd unit to set get the application config at runtime prior to starting the app. They are more most useful with Distillery.
Elixir 1.9+ mix releases support
runtime configuration
via config/releases.exs and rel/env.sh.eex. It is more secure, however, to
separate the process of getting configuration from the app itself using
ExecStartPre]).
See mix_systemd for examples.
deploy-sync-config-s3: Sync config files from S3 bucket to appconfiguration_dirdeploy-runtime-environment-file: Create#{runtime_dir}/environmentfile on target fromcloud-initmetadatadeploy-runtime-environment-wrap: Get runtime environment fromcloud-initmetadata, set environment vars, then launch main script.deploy-set-cookie-ssm: Get Erlang VM cookie from AWS SSM Parameter Store and write to file.
The most useful of these is deploy-sync-config-s3, the rest are code you might copy into
rel/env.sh.eex.
Dependencies
The generated scripts are mostly straight bash, with minimal dependencies.
deploy-sync-config-s3uses the AWS CLI to copy files from an S3 bucket.deploy-runtime-environment-fileanddeploy-runtime-environment-wrapuse jq to parse the cloud-init JSON file.deploy-set-cookie-ssmuses the AWS CLI andjqto interact with Systems Manager Parameter Store.
To install jq on Ubuntu:
apt-get install jq
To install the AWS CLI from the OS package manager on Ubuntu:
apt-get install awscli
Scenarios
CodeDeploy
The library can generate lifecycle hook scripts for use with a deployment system such as AWS CodeDeploy.
config :mix_deploy,
app_user: "app",
app_group: "app",
templates: [
"stop",
"create-users",
"create-dirs",
"clean-target",
"extract-release",
"set-perms",
"migrate",
"enable",
"start",
"restart",
],
...
Here is an example appspec.yml file:
version: 0.0
os: linux
files:
- source: bin
destination: /srv/foo/bin
- source: systemd
destination: /lib/systemd/system
- source: etc
destination: /srv/foo/etc
hooks:
ApplicationStop:
- location: bin/deploy-stop
timeout: 300
BeforeInstall:
- location: bin/deploy-create-users
- location: bin/deploy-create-dirs
- location: bin/deploy-clean-target
AfterInstall:
- location: bin/deploy-extract-release
- location: bin/deploy-set-perms
- location: bin/deploy-enable
ApplicationStart:
- location: bin/deploy-migrate
runas: app
timeout: 300
- location: bin/deploy-start
timeout: 3600
# ValidateService:
- location: bin/validate-service
timeout: 300
Staging
By default, the scripts deploy the scripts as the same OS user that runs the
mix deploy.generate command, and run the app under an OS user with the same
name as the app.
Many scripts allow you to override environment variables at execution time. For
example, you can override the user accounts which own the files by setting the
environment vars APP_USER, APP_GROUP, and DEPLOY_USER.
Similarly, set DESTDIR and the copy script will add a prefix when copying
files. This lets you copy files to a staging directory, tar it up, then extract
it on a target machine, e.g.:
mkdir -p ~/tmp/deploy
DESTDIR=~/tmp/deploy bin/deploy-create-dirs
DESTDIR=~/tmp/deploy bin/deploy-copy-files
Configuration options
The following sections describe common configuration options.
See lib/mix/tasks/deploy.ex for details of more obscure options.
If you need to make changes not supported by the config options,
then you can check the templates in rel/templates/deploy
into source control and make your own changes. Contributions are welcome!
The list of templates to generate is in the templates config var.
You can modify this list to remove scripts, and they won't be generated.
You can also add your own scripts and they will be run as templates with the
config vars defined.
Basics
app_name: Elixir application name, an atom, from the app field in the mix.exs project.
version: version field in mix.exs project.
module_name: Elixir camel case module name version of app_name, e.g. FooBar.
release_name: Name of release, default app_name.
ext_name: External name, used for files and directories,
default app_name with underscores converted to "-", e.g. foo-bar.
service_name: Name of the systemd service, default ext_name.
release_system: :mix | :distillery, default :mix
Identifies the system used to generate the releases, Mix or Distillery.
Users
deploy_user: OS user account that is used to deploy the app, e.g. own the
files and restart it. For security, this is separate from app_user, keeping
the runtime user from being able to modify the source files. Defaults to the
user running the script, supporting local deploy. For remote deploy, set this
to a user like deploy or same as the app user.
deploy_group: OS group account, default deploy_user.
app_user: OS user account that the app should run under. Default deploy_user.
app_group: OS group account, default deploy_group.
Directories
base_dir: Base directory for app files on target, default /srv.
deploy_dir: Directory for app files on target, default #{base_dir}/#{ext_name}.
We use the
standard app directories,
for modern Linux systems. App files are under /srv, configuration under
/etc, transient files under /run, data under /var/lib.
Directories are named based on the app name, e.g. /etc/#{ext_name}.
The dirs variable specifies which directories the app uses.
By default, it doesn't set up anything. To enable them, configure dirs, e.g.:
dirs: [
# :runtime, # App runtime files which may be deleted between runs, /run/#{ext_name}
# :configuration, # App configuration, e.g. db passwords, /etc/#{ext_name}
# :state, # App data or state persisted between runs, /var/lib/#{ext_name}
# :cache, # App cache files which can be deleted, /var/cache/#{ext_name}
# :logs, # App external log files, not via journald, /var/log/#{ext_name}
# :tmp, # App temp files, /var/tmp/#{ext_name}
],
Recent versions of systemd (since 235) will create these directories at
start time based on the settings in the unit file. For earlier systemd
versions, deploy-create-dirs will create them.
For security, we set permissions to 750, more restrictive than the systemd
defaults of 755. You can configure them with variables like
configuration_directory_mode. See the defaults in
lib/mix/tasks/deploy.ex.
systemd_version: Sets the systemd version on the target system, default 235.
This determines which systemd features the library will enable. If you are
targeting an older OS release, you may need to change it. Here are the systemd
versions in common OS releases:
- CentOS 7: 219
- Ubuntu 16.04: 229
- Ubuntu 18.04: 237
Additional directories
The library uses a directory structure under deploy_dir which supports
multiple releases, similar to Capistrano.
scripts_dir: deployment scripts which e.g. start and stop the unit, defaultbin.current_dir: where the current Erlang release is unpacked or referenced by symlink, defaultcurrent.releases_dir: where versioned releases are unpacked, defaultreleases.flags_dir: dir for flag files to trigger restart, e.g. whenrestart_methodis:systemd_flag, defaultflags.
When using multiple releases and symlinks, the deployment process works as follows:
-
Create a new directory for the release with a timestamp like
/srv/foo/releases/20181114T072116. -
Upload the new release tarball to the server and unpack it to the releases dir
-
Make a symlink from
/srv/#{ext_name}/currentto the new release dir. -
Restart the app.
If you are only keeping a single version, then deploy it to the directory
/srv/#{ext_name}/current.
Variable expansion
The following variables support variable expansion:
expand_keys: [
:env_files,
:env_vars,
:runtime_environment_service_script,
:conform_conf_path,
:pid_file,
:root_directory,
:bin_dir,
]
You can specify values as a list of terms, and it will look up atoms as keys in
the config. This lets you reference e.g. the deploy dir or configuration dir without
having to specify the full path, e.g. ["!", :deploy_dir, "/bin/myscript"] gets
converted to "!/srv/foo/bin/myscript".
Environment vars
Config vars set a few common env vars:
mix_env: defaultMix.env(), setsMIX_ENVenv_lang: defaulten_US.utf8, used to setLANG
In addition, you can set env_vars and env_files the same way
as for mix_systemd. The set-env script will then set these
variables the same way as they are in the systemd unit,
allowing you to run release commands with the same config, e.g. database
migrations or console. It also sets:
RUNTIME_DIR:runtime_dir, if:runtimeindirsCONFIGURATION_DIR:configuration_dir, if:configurationindirsLOGS_DIR:logs_dir, if:logsindirsCACHE_DIR:cache_dir, if:cacheindirsSTATE_DIR:state_dir, if:stateindirsTMP_DIR:tmp_dir, if:tmpindirs
You can set additional vars using env_vars, e.g.:
env_vars: [
"PORT=8080",
]
You can also reference the value of other parameters by name, e.g.:
env_vars: [
["RELEASE_TMP=", :runtime_dir],
]
You can read environment vars from files with env_files, e.g.:
env_files: [
["-", :deploy_dir, "/etc/environment"],
["-", :configuration_dir, "environment"],
["-", :runtime_dir, "environment"],
],
The "-" at the beginning makes the file optional, the system will start without them. Later values override earlier values, so you can set defaults in the release which get overridden in the deployment or runtime environment.
With Distillery, you can generate a file under the release with an overlay in
rel/config.exs, e.g.:
environment :prod do
set overlays: [
{:mkdir, "etc"},
{:copy, "rel/etc/environment", "etc/environment"},
# {:template, "rel/etc/environment", "etc/environment"}
]
end
That results in a file that would be read by:
env_files: [
["-", :current_dir, "/etc/environment"],
],
Starting and restarting
The following variables set systemd variables:
service_type: :simple | :exec | :notify | :forking. systemd
Type, default :simple.
Modern applications don't fork, they run in the foreground and
rely on the supervisor to manage them as a daemon. This is done by setting
service_type to :simple or :exec. Note that in simple mode, systemd
doesn't actually check if the app started successfully, it just continues
starting other units. If something depends on your app being up, :exec may be
better.
Set service_type to :forking, and the library sets pid_file to
#{runtime_directory}/#{app_name}.pid and sets the PIDFILE env var to tell
the boot scripts where it is.
The Erlang VM runs pretty well in foreground mode, but traditionally runs as as a standard Unix-style daemon, so forking might be better. Systemd expects foregrounded apps to die when their pipe closes. See https://elixirforum.com/t/systemd-cant-shutdown-my-foreground-app-cleanly/14581/2
restart_method: :systemctl | :systemd_flag | :touch, default :systemctl
The normal situation is that the app will be restarted using e.g.
systemctl restart foo.
With :systemd_flag, an additional systemd unit file watches for
changes to a flag file and restarts the main unit. This allows updates to be
pushed to the target machine by an unprivileged user account which does not
have permissions to restart processes. Touch the file #{flags_dir}/restart.flag
and systemd will restart the unit. See mix_systemd for details.
With :touch, the app itself watches the file #{flags_dir}/restart.flag.
If it changes, the app shuts itself down, relying on systemd to notice and restart it.
sudo_deploy: Creates /etc/sudoers.d/#{ext_name} file which allows the deploy
user to start/stop/restart the app using sudo. Default false. Note that
when you must call systemctl with the full path, e.g. sudo /bin/systemctl restart foo
for this to work.
sudo_app: Creates /etc/sudoers.d/#{ext_name} file allowing the app user
user to start/stop/restart the app using sudo. Default false.
Configuration examples
Here is a complete example of configuring an app from a config file which it pulls from S3 on startup.
We set up an ExecStartPre command in the systemd unit file which runs
deploy-sync-config-s3 before starting the app. It runs the AWS cli command:
aws s3 sync "s3://${CONFIG_S3_BUCKET}/${CONFIG_S3_PREFIX}" "${CONFIG_DIR}/"
CONFIG_S3_BUCKET is the source bucket, and CONFIG_S3_PREFIX is an optional
path in the bucket. CONFIG_DIR is the app configuration dir on the target
system, /etc/foo.
We need to bootstrap the config process, so we use a different environment file from the main config.
mkdir -p rel/etc
echo "CONFIG_S3_BUCKET=cogini-foo-dev-app-config" >> rel/etc/environment
Set exec_start_pre in the mix_systemd config:
config :mix_systemd,
app_user: "app",
app_group: "app",
# systemd runs this before starting the app as root
exec_start_pre: [
["!", :deploy_dir, "/bin/deploy-sync-config-s3"]
],
dirs: [
# Create /etc/foo
:configuration,
# Create /run/foo
:runtime,
],
# systemd should not clean up /run/foo
runtime_directory_preserve: "yes",
# Load env from /srv/foo/etc/environment and /etc/foo/environment
env_files: [
["-", :deploy_dir, "/etc/environment"],
["-", :configuration_dir, "/environment"],
],
# deploy-copy-files will copy the env file to /srv/foo/etc
# more likely it is done by e.g. appspec.yml
copy_files: [
%{
src: "rel/etc/environment",
dst: [:deploy_dir, "/etc"],
user: "$DEPLOY_USER",
group: "$APP_GROUP",
mode: "640"
},
],
env_vars: [
# Temp files are in /run/foo
["RELEASE_TMP=", :runtime_dir],
]
config :mix_deploy,
app_user: "app",
app_group: "app"
templates: [
"init-local",
"create-users",
"create-dirs",
"copy-files",
"enable",
"release",
"restart",
"rollback",
"start",
"stop",
"sync-config-s3",
],
dirs: [
:configuration,
:runtime,
],
# Set env config in e.g. deploy-set-env to match above.
env_files: [
["-", :deploy_dir, "/etc/environment"],
["-", :configuration_dir, "/environment"],
]
env_vars: [
["RELEASE_TMP=", :runtime_dir],
]
For security, the app only has read-only access to its config files, and
/etc/foo has ownership deploy:foo and mode 750. We prefix the command
with "!" so it runs with elevated permissions, not as the foo user.
We need to set the CONFIG_S3_BUCKET variable in the environment so that
deploy-sync-config-s3 can use it. We can set it in env_vars
or put it in the file /etc/foo/environment.
/srv/foo/etc/environmentsettings are configured at deploy time./etc/foo/environmentsettings might come from an S3 bucket./run/foo/environmentsettings might be generated dynamically, e.g. getting the IP address.
For example, post_build commands in the CodeBuild CI buildspec.yml file
can generate a config file files/etc/environment:
post_build:
commands:
- mkdir -p files/etc
- echo "CONFIG_S3_BUCKET=$BUCKET_CONFIG" >> files/etc/environment
Then the CodeDeploy appspec.yml copies it to the target system under /srv/foo/etc:
files:
- source: bin
destination: /srv/foo/bin
- source: systemd
destination: /lib/systemd/system
- source: etc
destination: /srv/foo/etc
See mix_systemd for more examples.
I am jakemorrison on on the Elixir Slack and Discord, reachfh on
Freenode #elixir-lang IRC channel. Happy to chat or help with
your projects.
Copyright and License
Copyright (c) 2019 Jake Morrison
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.