Marathon deployment automation tool
Clone or download
rasjoh Merge pull request #58 from meltwater/slack
Add slack support for notifications
Latest commit b961ac4 Jul 5, 2018

README.md

Lighter

Travis CI Coverage Status

Lighter solves the problem of automating deployments to Marathon and handling of differences between multiple environments. Given a hierachy of yaml files and environments, Lighter can expand service config files and deploy them to Marathon.

For even tighter integration into the development process, Lighter can resolve Marathon config files from Maven and merge these with environment specific overrides. This enables continuous deployment whenever new releases or snapshots appear in the Maven repository. Optional version range constraints allows patches/minor versions to be rolled out continuously, while requiring a config change to roll out major versions.

Usage

usage: lighter COMMAND [OPTIONS]...

Marathon deployment tool

positional arguments:
  {deploy,verify}       Available commands
    deploy              Deploy services to Marathon
    verify              Verify and generate Marathon configuration files

optional arguments:
  -h, --help            show this help message and exit
  -n, --noop            Execute dry-run without modifying Marathon [default:
                        False]
  -v, --verbose         Increase logging verbosity [default: False]
  -t TARGETDIR, --targetdir TARGETDIR
                        Directory to output rendered config files
  -p PROFILES, --profile PROFILES
                        Extra profile file(s) to be merged with service
                        definitions.

Deploy Command

usage: lighter deploy [OPTIONS]... YMLFILE...

Deploy services to Marathon

positional arguments:
  YMLFILE               Service files to expand and deploy

optional arguments:
  -h, --help            show this help message and exit
  -m MARATHON, --marathon MARATHON
                        Marathon URL like "http://marathon-host:8080/".
                        Overrides default Marathon URL's provided in config
                        files
  -f, --force           Force deployment even if the service is already
                        affected by a running deployment [default: False]
  --canary-group CANARYGROUP
                        Unique name for this group of canaries [default: None]
  --canary-cleanup      Destroy canaries that are no longer present [default:
                        False]

Verify Command

usage: lighter verify YMLFILE...

Verify and generate Marathon configuration files

positional arguments:
  YMLFILE           Service files to expand and deploy

optional arguments:
  -h, --help        show this help message and exit
  --verify-secrets  Fail verification if unencrypted secrets are found
                    [default: False]

Configuration

Given a directory structure like

config-repo/
|   globals.yml
|   myprofile.yml
└─ production/
|   |   globals.yml
|   |   myfrontend.yml
|   └─ mysubsystem/
|          globals.yml
|          myservice-api.yml
|          myservice-database.yml
└─ staging/
    |   globals.yml
    |   myfrontend.yml

Running lighter deploy -p myprofile1.yml -p myprofile2.yml staging/myfrontend.yml will

  • Merge myfrontend.yml with environment defaults from config-repo/staging/globals.yml, config-repo/globals.yml, myprofile1.yml and myprofile2.yml
  • Fetch the json template for this service and version from the Maven repository
  • Expand the json template with variables and overrides from the yml files
  • Post the resulting json configuration into Marathon

Marathon

Yaml files may contain a marathon: section with a default URL to reach Marathon at. The -m/--marathon parameter will override this setting when given on the command-line.

globals.yml

marathon:
  url: 'http://marathon-host:8080/'

Maven

The maven: section specifies where to fetch json templates from which are merged into the configuration. For example

globals.yml

maven:
  repository: "http://username:password@maven.example.com/nexus/content/groups/public"

myservice.yml

maven:
  groupid: 'com.example'
  artifactid: 'myservice'
  version: '1.0.0'
  classifier: 'marathon'

The Maven 'classifier' tag is optional.

Dynamic Versions

Versions can be dynamically resolved from Maven using a range syntax.

maven:
  groupid: 'com.example'
  artifactid: 'myservice'
  version: '[1.0.0,2.0.0)'

For example

Expression Resolve To
[1.0.0,2.0.0) 1.0.0 up to but not including 2.0.0
[1.0.0,1.2.0] 1.0.0 up to and including 1.2.0
[1.0.0,2.0.0)-featurebranch 1.0.0 up to and including 1.2.0, only matches featurebranch releases
[1.0.0,1.2.0]-SNAPSHOT 1.0.0 up to and including 1.2.0, only matches SNAPSHOT versions
[1.0.0,2.0.0]-featurebranch-SNAPSHOT 1.0.0 up to and including 1.2.0, only matches featurebranch-SNAPSHOT versions
[1.0.0,] 1.0.0 or greater
(1.0.0,] Greater than 1.0.0
[,] Latest release version
[,]-SNAPSHOT Latest SNAPSHOT version

Freestyle Services

Yaml files may contain a service: tag which specifies a Marathon json fragment to use as the service configuration base for further merging. This allows for services which aren't based on a json template but rather defined exclusively in yaml.

myservice.yml

service:
  id: '/myproduct/myservice'
  container:
    docker:
      image: 'meltwater/myservice:latest'
  env:
    DATABASE: 'database:3306'
  cpus: 1.0
  mem: 1200
  instances: 1

Overrides

Yaml files may contain an override: section that will be merged directly into the Marathon json. The structure contained in the override: section must correspond to the Marathon REST API. For example

override:
  instances: 4
  cpus: 2.0
  env:
    LOGLEVEL: 'info'
    NEW_RELIC_APP_NAME: 'MyService Staging'
    NEW_RELIC_LICENSE_KEY: '123abc'

Deep Merge

An YAML and JSON file upwards-recursive deep merge is performed when parsing service definitions. Precedence is defined by the directory structure

  • myservice.yml has the highest precedence
  • globals.yml files are merged with decreasing precedency upwards in the directory structure
  • myservice-1.0.0-marathon.json if fetched from Maven has the lowest precedence

Lists, dicts and scalar values are deep merged

  • Dicts are deep merged, the result containing the union of all keys
  • Lists are appended together
  • Scalar values coalesce to the not-null value with highest precedence

The default behaviour is to append lists together, however specific list items can be overriden and deep merged using a dict with integer keys. For example

myservice-1.0.0-marathon.json

{
    "container": {
        "docker": {
            "portMappings": [
                {"containerPort": 8080, "servicePort": 1234},
                {"containerPort": 8081, "servicePort": 1235}
            ]
        }
    }
}

myservice-override-serviceport.yml

override:
  container:
    docker:
      portMappings:
        # Override service ports 1234,1235 with port 4000,4001
        0:
          servicePort: 4000
        1:
          servicePort: 4001

Non-string Environment Variables

Booleans, integers and floats in the env section are converted to strings before being posted to Marathon. Non-scalar environment variables like dicts and lists are deep merged and automatically serialized to json strings.

myservice.yml

override:
  env:
    intvar: 123
    boolvar: TRUE
    dictvar:
      mykey:
       - 1
       - 'abc'

Would result in a rendered json like

{
  "env": {
    "intvar": "123",
    "boolvar": "true",
    "dictvar": "{\"mykey\": [1, \"abc\"]}"
  }
}

Variables

Yaml files may contain an variables: section containing key/value pairs that will be substituted into the json template. All variables in a templates must be resolved or it's considered an error. This can be used to ensure that some parameters are guaranteed to be provided to a service. For example

variables:
  docker.registry: 'docker.example.com'
  rabbitmq.host: 'rabbitmq-hostname'

And used from the json template like

{
    "id": "/myproduct/myservice",
    "container": {
        "docker": {
            "image": "%{docker.registry}/myservice:1.2.3"
        }
    },
    "env": {
        "rabbitmq.url": "amqp://guest:guest@%{rabbitmq.host}:5672"
    }
}

Lighter also allows specifying environment variables as values in the configuration yaml files.

With the following configuration: myservice.yml

service:
  id: '/myproduct/myservice'
  container:
    docker:
      image: 'meltwater/myservice:%{env.VERSION}'
  env:
    DATABASE: 'database:3306'
  cpus: 1.0
  mem: 1200
  instances: 1

And Running VERSION=1.1.1 lighter deploy myservice.yml, lighter will deploy the docker image meltwater/myservice:1.1.1 to marathon.

To avoid interpolating some string like %{id} when you really want it, use %%{id}

Predefined Variables

Variable Contains
%{lighter.version} Maven artifact version or Docker image version
%{lighter.uniqueVersion} Unique build version resolved from Maven/Docker metadata

Snapshot Builds

If an image is rebuilt with the same Docker tag, Marathon won't detect a change and hence won't roll out the new image. To ensure that new snapshot/latest versions are deployed use %{lighter.uniqueVersion} and forcePullImage like this

myservice.yml

override:
  container:
    docker:
      forcePullImage: true
  env:
    SERVICE_BUILD: '%{lighter.uniqueVersion}'

Docker Registry

Lighter calls the Docker Registry API to resolve %{lighter.uniqueVersion} when it's used in a non-Maven based service. This is only enabled if the %{lighter.uniqueVersion} variable is actually referenced from the service config.

For authenticated reprositories you must supply read-access credentials to be used when calling the registry API. You can find the base64 encoded credentials in your ~/.docker/config.json or ~/.dockercfg files. Note that Docker Hub is not supported at this time.

globals.yml

docker:
  registries:
    'registry.example.com':
      auth: 'dXNlcm5hbWU6cGFzc3dvcmQ='

Facts

Yaml files may contain a facts: section with information about the service surroundings

staging/globals.yml

facts:
  environment: 'staging'

Secrets Management

Lighter has support for Secretary which can securely distribute secrets to containers.

*someenv/globals.yml *

secretary:
  url: 'https://secretary-daemon-loadbalancer:5070'
  master:
    publickey: 'someenv/keys/master-public-key.pem'

someenv/myservice.yml

override:
  env:
    DATABASE_PASSWORD: "ENC[NACL,NVnSkhxA010D2yOWKRFog0jpUvHQzmkmKKHmqAbHAnz8oGbPEFkDfyKHQHGO7w==]"

Note

Make sure the environment variable name is a valid shell script identifier and supported by Secretary, only alphanumeric characters and underscores are supported, starting with an alphabetic or underscore character. e.g DATABASE.PASSWORD is invalid but DATABASE_PASSWORD is valid.

Canary Deployments

Lighter together with Proxymatic supports canary deployments using the --canary-group parameter. This parameter makes Lighter rewrite the app id and servicePort to avoid conflicts and automatically add the metadata labels that Proxymatic use for canaries. The --canary-cleanup parameter destroys canary instances when they are removed from configuration.

Take care that the --canary-group parameter is unique to the deployment job and branch that executes the canary deployment. Lighter will clean out canaries with the same group name if they aren't being generated anymore, and if multiple deployment jobs share a group name they'd conflict and destroy each others canaries.

Canaries From Files

This example use a *-canary-* filename convention to separate canaries from normal services. In this workflow you would copy the regular service file myservice.yml, and make any tentative changes in this new myservice-canary-somechange.yml. When the canary has served its purpose you'd git mv back or git rm the canary file.

# Deploy regular services
lighter deploy -f -m "http://marathon-host:8080/" $(find . -name \*.yml -not -name globals.yml -not -name \*-canary-\*)

# Deploy and prune canaries
lighter deploy -f -m "http://marathon-host:8080/" --canary-group=generic --canary-cleanup $(find . -name \*-canary-\*.yml)

Canaries From Pull Requests

This usage would run lighter -t /some/output/dir verify ... on a PR and again on its base revision. Then diff -r the rendered json files to figure out what services were modifed in the PR. The modified services would be deployed as canaries with lighter deploy --canary-group=mybranchname --canary-cleanup ... whenever the PR branch is changed. When the PR is closed or merged the canaries would be destroyed using lighter deploy --canary-group=mybranchname --canary-cleanup

Canary Metrics

Lighter adds a Docker label com.meltwater.lighter.canary.group which can be used to separate out container metrics from the canaries.

Installation

Place a lighter script in the root of your configuration repo. Replace the LIGHTER_VERSION with a version from the releases page.

#!/bin/bash
set -e

LIGHTER_VERSION="x.y.z"

BASEDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
LIGHTER="$BASEDIR/target/lighter-`uname -s`-`uname -m`-${LIGHTER_VERSION}"

if [ ! -x "$LIGHTER" ]; then
    mkdir -p $(dirname "$LIGHTER")
    curl -sfLo "$LIGHTER" https://github.com/meltwater/lighter/releases/download/${LIGHTER_VERSION}/lighter-`uname -s`-`uname -m`
    chmod +x "$LIGHTER"
fi

# Ligher will write the expanded json files to /tmp/output
exec "$LIGHTER" -t "`dirname $0`/target" $@

Use the script like

cd my-config-repo

# Deploy/sync all services (from Jenkins or other CI/CD server)
./lighter deploy $(find staging -name \*.yml -not -name globals.yml)

# Deploy single services
./lighter deploy staging/myservice.yml staging/myservice2.yml

Integrations

Lighter can push deployment notifications to a number of services.

HipChat

Yaml files may contain an hipchat: section that specifies where to announce deployments. Create a HipChat V2 token that is allowed to post to rooms. The numeric room ID can be found in the room preferences in the HipChat web interface.

hipchat:
  token: '123abc'
  rooms:
    - '123456'

The default message of the hipchat notification is :

Deployed /my-image-id with image example-registry.io/image:1.0.0 to staging (marathon-url.example.com).

If you would like to post releases notes in addition to the above message, you have 2 options :

  1. You can add the following block to your service config
hipchat:
  releaseNotes: "my amazing release notes"
  1. You can add the release notes with a label on the docker image itself and then indicate which label lighter should use to get the release notes :

dockerfile

FROM registry.io/image:tag
LABEL com.example.component.release-notes="my amazing release notes"

service config

hipchat:
  message.image.label: "com.example.component.release-notes" # image label to use for hipchat message

Slack

Yaml files may contain an slack: section that specifies where to announce deployments. Create a Slack App Oauth token that is allowed to post to channels with chat:write:user, chat:write:bot scopes.

slack:
  token: 'xoxb-1234-56789abcdefghijklmnop'
  channels:
    - '#channel'

The default message of the slack notification is :

Deployed /my-image-id with image example-registry.io/image:1.0.0 to staging (marathon-url.example.com).

If you would like to post releases notes in addition to the default message, you have 2 options :

  1. You can add the following block to your service config
slack:
  releaseNotes: "my amazing release notes"
  1. You can add the release notes with a label on the docker image itself and then indicate which label lighter should use to get the release notes :

dockerfile

FROM registry.io/image:tag
LABEL com.example.component.release-notes="my amazing release notes"

service config

slack:
  message.image.label: "com.example.component.release-notes" # image label to use for slack message

New Relic

To send New Relic deployment notifications supply your New Relic REST API key (different from the license key given to the agent).

globals.yml

newrelic:
  token: '123abc'

myservice.yml

override:
  env:
    NEW_RELIC_LICENSE_KEY: 'abc123'
    NEW_RELIC_APP_NAME: 'MyService'

Datadog

To send Datadog deployment events supply your Datadog API key. Lighter will add Marathon appid and canary group as Docker container labels in order for Datadog to tag collected metrics, see: collect_labels_as_tags.

globals.yml

datadog:
  token: '123abc'
  tags:
    - subsystem:example

Datadog Puppet Config

datadog::docker:
  docker_daemon:
    instances:
      - url: "unix://var/run/docker.sock"
        new_tag_names: true
        collect_labels_as_tags: ["com.meltwater.lighter.appid", "com.meltwater.lighter.canary.group"]

Graphite

To send Graphite deployment events supply your Graphite plaintext and HTTP endpoints.

globals.yml

graphite:
  address: 'graphite-host:2003'
  url: 'http://graphite-host:80/'
  prefix: 'lighter'
  tags:
    - subsystem:example

Contributors