A tool for git and Docker deployments. A middle ground between simple Docker composition tools and full blown cluster orchestration.
The project is not battle tested in production yet.
The tool performs three functions (not necessarily all of them):
- Synchronizes local git repositori(es), possibly triggered by a webhook.
- Builds Docker images and pushes them to a Docker registry (optionally and experimentally with rocker).
- Runs Docker container pods (currently with rocker-compose).
deploy is meant to be used in an dev-staging-production environment, where the dev environment will sync application sources in response to git webhook. From these sources Docker images would be built and and pushed to a registry and finally the images can be deployed, without sources outside the containers, to staging and production. For overview tutorial of this process check here.
deploy's configuration is suited for managing more than a few projects:
- It is declarative and short to type.
- It supports splitting the configuration into multiple files (e.g. one per project).
- It supports reusable/extendable project templates.
- It supports project dependencies.
- One can create hollow projects only describing dependencies to other projects and in this way manage whole sever deployments with simple few word commands.
All functionality is available via CLI or REST interface and the REST server also handles webhooks. Currently the webhooks from GitHub are supported with GitLab on the way.
This tutorial is meant as illustration along these docs and to show the whole picture how deploy was intended to be used. It is not a sufficient source of information without the docs here.
-
Make some empty dir, e.g.
/myconfig
. -
Put your private SSH key
id_rsa
(if you need one) and your deploy configlocal.yml
in/myconfig
. If you experience problems with the key you can place a file calledDEBUG
in the config directory, this will enable the output of the ssh agent, which is normally suppressed. -
Make some dir where you will sync your projects, e.g.
/myapps
, it needs to be accessible inside the container, as well as all other dirs referenced in your config. -
Use from the CLI like (replace with your actual paths):
docker run --rm -ti \ -v /myapps:/myapps \ -v /myconfig:/app/config \ -v /var/run/docker.sock:/var/run/docker.sock \ -v /user/.docker:/root/.docker \ perennial/deploy:master \ deploy sync "*#*"
Explanation:
-v /myapps:/myapps
- this is somewhere to be able to do git clones and similar, doing it in the container makes little sense.-v /myconfig:/app/config
- this is the local config for the deploy app and the SSH private key for git.-v /var/run/docker.sock:/var/run/docker.sock
- this is so the container can access the Docker API.-v /user/.docker:/root/.docker
- this is so docker login will carry inside the container.
Replace
/user/.docker
with your actual user directory, if you need to push, or remove this line otherwise. The last two arguments (sync "*#*"
) is the actual deploy command, you can change it.Of course you will want to make this into a shell script for reuse, replacing the deploy arguments with
$@
and symlinking it in your path, so you can just typedeploy sync my#branch
... Or if you start it without arguments it will start a web server, but you will need to add port redirect like-p 80:80
. -
Or start an HTTP server
docker run --rm -ti -p 80:80 \ -v /myapps:/myapps \ -v /myconfig:/app/config \ -v /var/run/docker.sock:/var/run/docker.sock \ -v /user/.docker:/root/.docker \ perennial/deploy:master
Normally you will want to start this as a daemon, e.g.
-d --restart=aways
. -
Now you can use the REST interface to trigger commands or receive webhooks.
You need Node.js >= 4.0.0. If you don't have it you don't need to install it system wide. You can just download the
archive, extract it somewhere and use the node
command from the bin
directory there.
- Download the contents of this repo either from the zip button or from the releases section.
- Extract somewhere, open a shell and cd to that directory.
- You can start the app with
node deploy.js
or thedeploy
shell script.
Make sure you have git, docker, rocker and rocker-compose installed, so deploy can start them!
The CLI syntax is to pass one or more commands to be performed on a given project and given branch. Multiple commands are performed in the same order and are separated with commas, without space.
deploy <command[,command]..> <project[#branch]>.. [OPTIONS]..
Command is one or more of bellow:
The project can be given as literal name, which should match the one in the configuration, as repository or as *
.
The repository syntax is repo:host/user/repo
. For example
repo:github/Perennials/deploy
. Passing a repository for the project will
perform the command on all projects related to this repository. Notice the host
is not github.com
but just lowercase github
. This is the format used
everywhere throughout the configuration.
Passing *
will perform the command on all projects.
It is possible to pass multiple projects to perform the command on.
The branch can be given as as literal name or *
.
The branch can be ommited if a project is configured explicitly with a single
branch, that is not a regular expression. Passing *
will perform the command
on all enabled branches.
Remark: Using *
for a branch may use the repo
configuration of
the project. deploy will use the credentials from the config to find all
remote branches for this repo. *
can be used on a project without repo
configuration
or without credentials
configuration for the given repo
, but only for public
repos
or only for the branches
listed in the project configuration.
These are global options for all all commands. Specific commands may have additional options.
--var <name>=<value>
- allows for overriding global variables. May occur multiple times.--config <pathname>
- allows for loading config(s) from custom file instead ofconfig/local.yml
. May occur multiple times and configs will be merged.-dry
- performs the command(s) in dry run mode, that is only display what is to be done without actually doing it. This has no effect for thelist
command because it is another form of dry run.-deps
- performs the commands including all dependencies for these commands.-no-deps-cache
- disable dependencies cache. This means normally, if project A and B are built in the same command and they both depend on project C being built, deploy will build C only once. This may not work in some circumstances if, for example, something else is executed in the mean time that does rmi on project C before starting to build project B. This flag disables this sort caching and will do all commands on all deps all the time.
deploy sync myproject#master
deploy clean,sync "myproject#*"
deploy sync repo:github/Perennials/deploy#master
deploy list "*#*"
Lists all matching projects and branches. Useful to test wildcards. If the
-deps
switch is given it will show the dependencies of the projects. The
-deps
switch can be given an argument to narrow down the dependencies only
for specific command (sync
, clean
, build
, push
, rmi
, run
, stop
).
deploy list <project[#branch]> [-deps[=command]]..
Git commands can be performed only on projects with repo configuration.
!!! Warning: this functionality is only intended for mirroring remote repositories automatically. It will make all effort to ensure that this will not fail, including doing hard reset and deleting conflicting untracked files. It should not be used in a real working copy.
!!! Warning: because of the above warning make sure you don't execute the command in a wrong local
directory (based on the repo config) because git reset
may cause loss of data.
The command will sync the repo for the specified project and branch. If the local copy does not exist,
the repo will be cloned recursively. If a local copy exists the remote will be pulled recursively.
The filter
flag can be used when multiple repos are specified in the config.
!!! Warning: all local changes and conflicts will be discarded without backup or confirmation.
deploy sync <project[#branch]> [-tag=tag] [-filter=repo]
Using the option -tag
one can specify to synchronize to a specific tag in the repo. This will work
only for repos without explicitly specified branch in the remote URL. E.g. if the repo in the config
is specified as 'github/Perennials/deploy#master', adding a -tag
option will still synchronize the
tip of the master branch. But if the remote repo is specified as github/Perennials/deploy
, then the
-tag
option can be used to sync to specific tag. The reason for this behaviour is that a project
may declare multiple repos and they will rarely have their tags synchronized. So the option to sync
a tag is meant to be used on the main repo of the project, since it will usually match the project
branch and it will not be explicitly specified in the remote URL.
The command will remove all local repositories for the project without backup or confirmation.
Projects with label dont-clean
will be skipped, unless the -force
flag is passed.
The filter
flag can be used when multiple repos are specified in the config.
deploy clean <project[#branch]> [-force] [-filter=repo]
Image commands can be performed only on projects with Image configuration
The command will build the Docker image(s) for the specified project and branch.
deploy build <project[#branch]> [-pull] [-no-cache] [-debug-image] [-push] [-attach] [-filter=image]
-pull
and -no-cache
is passed to both docker and rocker as --pull
and --no-cache
.
The -debug-image
flag is used with only with rocker to print the Rockerfile
after
the variables has been substitued.
-attach
and -push
are passed to rocker only as --atach
and --push
.
The command will push the Docker image(s) for the specified project and branch.
The filter
flag can be used when multiple images are specified, it can include wildcards.
deploy push <project[#branch]> [-filter=image]
The command will remove the Docker image(s) for the specified project and branch.
Projects with label dont-rmi
will be skipped, unless the -force
flag is passed.
-force
will also carry to the Docker rmi
command. The filter
flag can be used
when multiple images are specified, it can include wildcards.
deploy rmi <project[#branch]> [-force] [-filter=image]
Pod commands can be performed only on projects with pod configuration.
The command will run the Docker container(s) pod for the specified project and branch. All non-existing host directories that are to be bound as container volumes will be created prior to launching the pod.
deploy run|start <project[#branch]> [-debug-pod[=more]] [--cmd command]
-debug-pod
will print the rendered pod definition which is passed to
rocker-compose. Adding =more
will print even more info from rocker-compose.
--cmd
is a special case and if passed it will carry to a variable called cmd
in
rocker-compose, so it can be used in the pod definition to customise the container command.
The command will run the Docker container(s) pod for the specified project and branch.
deploy stop <project[#branch]> [-debug-pod]
-debug-pod
will print the rendered pod definition which is passed to
rocker-compose.
Starting the script without arguments will create an HTTP server according to the configuration and listen for REST requests or webhooks.
deploy
http://myserver.com/<action>[/project[/branch]][?secret=secret-access[&flag]..]
The syntax for action
, project
and branch
is the same in the CLI interface, with the
addition of the deploy
action which has special purpose for webhooks. secret-access
may
be used if it is allowed in the configuration.
http://myserver.com/sync/myproject/master
http://myserver.com/clean,sync/myproject/*?rmi&force
Use URL like this for the webhook configuration:
http://myserver.com/deploy
Or just pass any REST URL as webhook. This is actually the REST URL for the
deploy
command. The deploy
command has special purpose and is only
available for webhooks. It means to detect the command from the webhook
payload. In other words it will perform either sync
or clean
depending if
you push something in the branch or delete the branch.
With this syntax the project and the branch will also be auto detected from the webhook payload.
Otherwise you can force specific action or specific project and/or branch. If the project or the branch is omitted it will be auto detected from the payload. For example:
http://myserver.com/sync/myproject/master?secret=itsme
- If you want HTML output send header
Accept: text/html
. - If you want plain text output with ANSI colors send header
Accept: text/tty
. - Otherwise you will get plain text without colors.
The configuration is read from config.yml
and optionally merged with
config/local.yaml
. Or optionally, with the --config
CLI argument, from any
file.
The project configuration is in flexible YAML format provided by js-yaml. The pod definitions are in YAML format accepted by rocker-compose.
Besides the custom elements provided by js-yaml,
which enable specifying JavaScript regular expressions and functions inside the configuration,
deploy has several custom types of itself. All of these can be used anywhere and will
be evaluated only when they are need. For example using a !!js/function
for the value of
a variable, will evaluate the function only when the variable needs to be substituted in a
given context. The custom types provided by deploy are:
Property | Description |
---|---|
!cmd command |
Will execute a shell command and use its output as a value. |
!echo text |
Will echo the text to the console. |
!yamlfile file |
Will parse a YAML file and incorporate its contents in the document. |
`!yamlfiles concat | merge pattern` |
!textfile file |
Will read a file as a plain text and use it as a value. |
!if ... |
Conditional markup. See bellow. |
!deploy command project#branch |
Performs a deploy command on the specified project and branch. E.g. !deploy sync myproject#1.0 . The branch can be omitted if it can be inferred from the config. The project and branch syntax here only supports literal project and branch name, no wildcards or repos like on the CLI. |
It is possible to use the !if
custom YAML element to create different subtree based on a condition. The syntax is follows:
!if >
/condition1 operator condition2/
# then document
/else/
# else document
condition1
andcondition2
can be a YAML value, including variables.operator
could be==
or!=
.then document
andelse document
is YAML structure.- The
else
block is optional.
Example:
Lets assume we have a dev environment and we want to bind some directory as
volume only on the dev environment. We can place the dependency in an !if
and the markup won't be include in different environment.
deps:
run: !if >
/${env} == 'dev'/
providerkit#{project.sdk.version}: sync
The configuration has three main sections, all placed in the root of the config file.
Presented bellow is the default configuration for the REST interface. It only allows connections from GitHub and localhost. If you want to access it remotely you either need to add your IP in the known hosts, or enable and pass a secret when connecting.
http:
host: 0.0.0.0
port: 80
known-hosts:
localhost: 127.0.0.1
github: 192.30.252.0/22
# secret-access: 'itsme'
Property | Description |
---|---|
http.host |
IP address to listen. 0.0.0.0 will listen on all available addresses. |
http.port |
Port to listen on. You may need special access to listen on the default port 80 . |
known-hosts |
List of known hosts. Only IPs in this list are allowed access to the REST interface (or ones who know the secret, if it is enabled). |
secret-access |
A password to allow access from outside the known hosts. If it is an empty string '' all access is allowed without providing a secret. If it is not specified, i.e. commented like the default, secret access is disabled. |
Git commands require git
to be installed and properly configured with SSH
key (if you plan to use private repositories). For more information check the
tutorial on GitHub.
Additionally, to be able to perform bulk commands, that is commands on
multiple branches, depending on your configuration deploy may need to use
the web APIs to determine the available branches. For this to work
credentials
configuration must be supplied.
It is not mandatory, but without it you will not be able to execute commands
like deploy sync myproject#"*"
, or it will work only for manually defined
branches.
You can authenticate for GitHub either with your username and password or with access token. If both are supplied token
will be preferred. You can give multiple authentications for different users or organisations.
credentials:
github:
user:
token: authentication_token
username: username
password: password
Property | Description |
---|---|
credentials.github.user |
Replace user with the actual GitHub user or organisation who's repositories these credentials will grant access to. It does not need to be the same as the username . Case sensitive, must match the repo configuration, e.g. for repo github/Perennials/deploy the user here should be Perennials , but the username may be any user with access to the repo. |
There are two sections in the root of the config file related to the project configuration.
Just a list of global variables that can be reused elsewhere in the
configuration. To use the variable use the {variable_name}
or
${variable_name}
syntax anywhere. Variables will be resolved when they are
actually used, not at the time of loading the config. If a variable is used as
sole value of some node, it should be quoted or prepended with $
to
disambiguate the YAML mapping syntax.
vars:
name: value
The following global variables will be predefined.
Variable | Description |
---|---|
{deploy.root} |
The root directory where the application itself resides. |
Sometimes it is useful to be able to debug the values of your variables. If
you place a variable debug: true
the variables (either global or project)
will be printed after they are loaded.
You can use something like deploy debug --var debug=true --var project.debug=true
.
Debug is not valid command so the program will terminate. Of course you could use real command.
vars:
# defines a global variable named env with value dev
env: dev
# reference to the variable {env} inside another value, will be replaced with dev
apps.root: /{env}/apps/
# custom elements, this will execute the command id when this variable is substitued
username: !cmd id -u -n
# if we don't use the quotes here YAML will interpret it as mapping
# this will likely lead to errors
env.ref: '{env}'
Projects are described in the projects
root node. Projects can be reused as
templates. All project sub-configuration is optional.
Projects is either a mapping or an array of mappings. Additionally if the array items are mappings with one key it will be assumed to be a name of the project.
It is possible to have different projects with the same, e.g. to have different configuration for different branches. To make use of this either use an array or put the name of the branch in the project (see bellow).
Mapping:
projects:
foo:
extends: other_project
template: true
labels: just some labels
branches:
# enabled branches for the project
vars:
# project specific variables
events:
# project specific event handlers
deps:
# project dependencies
repo:
# repo configuration for the project
image:
# docker image configuration for the project
pod:
# pod configuration for the project
bar:
extends: ...
#...
Array of mappings. In this case the name should be given as property:
projects:
- name: foo
extends: ...
#...
- name: bar
extends: ...
#...
Array of mappings with one key (used as a name):
projects:
- foo:
extends: ...
branches: ...
#...
- bar:
extends: ...
branches: ...
#...
If the name of the project contains a branch name, the branch will be appended to the list of branches. This is to support different configurations for branches that may be too different.
projects:
foo#1.0:
#...
foo#2.0:
#...
Property | Value type | Description |
---|---|---|
name |
string | The name of the project. |
extends |
string | A template to use for the base of this project. The properties of this project will be merged recursively with the template. |
template |
true |
Indicates the project is a template to be used for base of other projects and should be excluded of normal project treatment. |
labels |
string | A space separated list of labels. Currently only the labels dont-clean and dont-rmi has any use - to protect projects from being cleaned by mistake when cleaning with wildcard. |
branches |
string|string[] | Enabled branches for the project. You can specify one or multiple branches. Commands on branches outside of this list will be ignored. The default is * , which means all branches are enabled. The js-yaml !!js/regexp custom type can be used here. |
vars |
mapping | A list of project specific variables. The same as in the root section but all names will only be available in the context of the project, not globally. |
events |
mapping | A list of project specific event handlers. These are some commands that will be executed upon some event (see bellow). |
deps |
mapping | A list of project dependencies. These are some projects and their commands that will be executed as prerequisite for performing a command on the project (see bellow). Dependencies will be ignored if the -deps flag is not given. |
repo |
mapping|mapping[] | Repo configuration for the project. See bellow. |
image |
mapping|mapping[] | Docker image configuration for the project. See bellow. |
pod |
mapping | Pod configuration for the project. See bellow. |
Besides the global variables defined in the root of the configuration, the following variables will available in the scope of the project.
Variable | Description |
---|---|
{project} |
Name of the current project. |
{branch} |
Name of the current branch. |
Projects support events. Events are executed before and after each command. The name of the event is composed from the name of the command (sync
, clean
, build
, push
, rmi
, run
, stop
) and a suffix:
Event | Description |
---|---|
command.start |
Will be fired before a command is performed. |
command.error |
Will be fired after a command is performed if it results in error. |
command.success |
Will be fired after a command is performed if it results in success. |
command.finish |
Will be fired after a command is performed, regardless if the command resulted in success or error. This will be fired after the .success or .error event. |
Each event can have one or multiple handlers, for example lets assume we want to temporarily copy some dependency inside the project directory, just during the build process so it will be available for Docker:
events:
build.start:
!cmd cp -rf {project.sdk} {project.local}/lib/sdk
build.finish:
- !cmd ls -l {project.local}/lib/sdk
- !cmd rm -rf {project.local}/lib/sdk
Projects can declare dependencies with other projects. It is possible to
create dummy projects that does nothing but declare dependencies and this way
group projects logically. Dependencies will only be executed if the -deps
CLI flag is given.
Dependencies are given per command or for all commands. All sections are optional.
The #branch
is also optional if it is possible to infer it, i.e. if the project
has only one static branch defined.
deps:
all:
- project1#branch
- project2#branch
# ...
command:
all: command1, command2
project#branch: command1, command2
project#branch: command1, command2
Event | Type | Description |
---|---|---|
all |
string[] | Array of projects and they branches that will be inserted as dependency for each command. The subcommand, that is the command executed on the dependency will be the same as the command on the main project. |
command : mapping |
command should be replace with the actual command, i.e. one of sync , clean , build , push , rmi , run , stop , skip . Each key of the mapping is a dependency project with its branch. The value is either an array if commands to be performed on the dependency or string that is command separated list of commands. The all key has special meaning - it is not a project but can be used to override the command(s) that will be performed on the projects in the all section, instead of performing the same command as the main project. The skip command has special meaning - it is used to skip a project that was defined in all from a specific command. |
Example:
In this example if we execute deploy build fullsystem
, this will perform
build on base#images
, ws1#1.1
, ws2#1.2
, ws3#1.3
and ws4#master
. And
if we perform run this will perform build
and run
on the same projects.
Performing clean will skip the project base#images
. One can use the list
command with -deps
or test with -dry
to test dependencies.
projects:
fullsystem#master:
deps:
all:
- base#images
- services
- suppliers
run:
all: build, run
clean:
base#images: skip
services#master:
deps:
all:
- ws1#1.1
- ws2#1.2
- ws3#1.3
suppliers#master:
deps:
all:
- ws4#master
This configuration is mandatory for the git commands. It is a
mapping where the key is the remote repository and the value is the local
directory where the remote will be synced. The mapping may contain multiple
repos, but the first one is considered a main one and will be used when
determining the branches for the project in some cases. The branch part of the
remote is optional for the main (the first) repository and if ommited it is
the same as if the variables {branch}
is used, in otherwords the current
branch will be used. The host in the remote is given only with its name, e.g.
github/Perennials/deploy
.
repo:
host/user/repo#branch:local_directory
Describes the Docker image(s) for this project. Can be a mapping or array of mappings.
image:
image: name:tag
path: build_path
file: build_file
rocker: bool
vars:
## Rockerfile template variables
The image
will be used for the build
and push
actions. path
and file
will be used only for building.
Property | Value type | Description |
---|---|---|
image.image |
string | Mandatory. Name and tag for the docker image of this project. |
image.path |
string | Mandatory for build . Build path for the Docker image. Can be local path or URL as accepted by Docker build. |
image.file |
string | For specifying a custom Dockerfile or Rockerfile relative to the build path. The default is Dockerfile or Rockerfile if rocker is used. |
image.rocker |
bool | If to use rocker instead of docker for building the image. The default is false . This does not need to be specified explictly when a file is specified and it is named Rockerfile . |
image.vars |
mapping | A set of variables to be substituted inside the Rockerfile file according to the rules of rocker. |
Describes the container pod configuration for the project. In the current implementation rocker-compose is used to start and stop the pod of containers, therefore the pod is described in a separate YAML file in the format accepted by rocker-compose.
pod:
path: pod_definition_path
file: pod_definitnion_file
vars:
## pod definition template variables
Property | Value type | Description |
---|---|---|
pod.path |
string | Mandatory. Path to the pod definition file. |
pod.file |
string | For specifying a custom pod definition file relative to the build path. The default is compose.yml . |
pod.vars |
mapping | A set of variables to be substituted inside the pod definition file according to the rules of rocker-compose. |
For more examples check the examples folder. Start with local.yml
in each folder.
You can also check the tutorial.
ws2:
## sync only the master branch or two digit semver branches like 1.1
branches: [ 'master', !!js/regexp '^\d+\.\d+$' ]
extends: base
repo:
### will sync from this repo to this local directory
github/Perennials/ws2: '{project.local}'
image:
### name of docker image
image: perennial.custom.registry/{project}:{branch}
### path for building the image
path: '{project.local}'
### Dockerfile relative to the path
file: docker/Dockerfile
base:
template: true
vars:
project.local: /apps/{project}/{branch}
Borislav Peev (borislav.asdf at gmail dot com) Tino Jahnke (tino.jahnke at web dot de) Jan Müller (to be filled)