Docker Integration

As of version 5, Corus offers Docker integration: Docker containers are managed through Corus' distribution and process management capabilities. Long story short: a Corus instance will control the Docker daemon on its host, and configure the launch of containers through the same way it does "native" processes. That is: containers are treated the same way as non-containerized processes, through a delegation mechanism that defers process lifecycle control to the Docker daemon.

To provide a better picture, here are capabilities presented by the Corus/Docker integration:

  • Configuration of port mappings - potentially hooked to Corus' port management functionality.
  • Automatic synchronization of images with the Docker registry upon distribtion deployment and undeployment.
  • Integration with Corus' process properties: allows passing such properties as environment variables to Docker containers, or defining volume mappings, for example.
  • Control of containers through Corus' process abstraction: exec, kill, restart, etc.
  • Control of distributed Docker daemon instances through Corus' command-line interface and REST API.
  • Complete integration with Corus tags: allows conditionally executing containers for given images, based on tagging.
  • Ability to eliminate dependency of the Docker registry as single point of failure, through the so-called "registry-less" mode.
  • A specfic REST API offering specific interactions useful in the context of the Corus/Docker integration.

Contents:

Introduction

Although, as explained further below, there are two "modes" that are supported in the context of the Corus/Docker integration, from a general vantage point, both amount to the same thing: the distribution abstraction (materialized through Corus' distribution descriptor) is used to parametrize the startup of Docker containers. At runtime, each container is represented within Corus as a process. This allows leveraging the entirety of Corus' distribution and process management capabilities, as if containers were native processes.

The two integration modes alluded to previously are the following:

  • Standard mode: in this context, each Corus node is responsible for triggering the required pulls from the Docker registry when a Docker-enabled distribution is deployed. This interaction is transparent to the client performing the deployment.
  • Registry-less mode: under this mode, Docker images are pushed (i.e.: "loaded", in Docker's terminology) to each Docker daemon from the client performing the deployment. The deployment of images in this case relies on Corus' cascading deployment functionality, and the Docker registry is not involved. This mode is meant to minimize the dependency on the Docker registry, which in fact is a single point of failure and requires redundancy in production in order to guarantee up time and scalability (in the presence of a potentially large number of Docker daemons).

Docker-Enabled Distributions

The corus.xml descriptor sports a <docker> element, which configures the startup parameters for a Docker container corresponding to a given image. The sample below illustrates a typtical Docker-based configuration:

<distribution name="memcached" version="1.0"
  xmlns="http://www.sapia-oss.org/xsd/corus/distribution-5.0.xsd">

  <process name="server" maxKillRetry="3" shutdownTimeout="300000" invoke="true">

    <port name="memcached" />
    
    <docker image="mini/memcached" profile="default">
      <portMapping hostPort="${corus.process.port.memcached}" containerPort="11211" />
      <env>
      	<property name="MEMCACHED_MEMORY" value="${memcached.mem.size}" />
      </env>
    </docker>

  </process>
</distribution>

Multiple attributes/nested elements are supported, as shown above. For example:

  • Corus' port management support can be leveraged in the context of Docker port mapping configuration.
  • Corus' process properties can be used as variables within the descriptor, as shown by usage of the <env> element in the above sample. In this case: a process property is used to set an environment variable that will be passed to the Docker container.

Given the above descriptor, a container for the mini/memcached image could be started as follows:

exec -d memached -v 1.0 -n server -p default -cluster

The above command would trigger the launch of a corresponding Docker container across all hosts in the cluster: upon receiving the command, a Corus node would detect the process as corresponding to a given Docker image (in the above case, mini/memcached), and simply interact with the Docker daemon on its host to start a container stemming from that image. Once that is done, Docker containers are managed through Corus' process abstraction, through commands such as ps, kill, restart, etc. (or through similar interactions offered by the REST API). A Corus node therefore acts as a proxy, delegating the execution of these commands to the Docker daemon on its host.

One important point consists of how Docker images are deployed to the Docker daemon on each host in the cluster: this depends on the integration mode, as mentioned earlier. The details pertaining to each mode are covered in the next sections.

Standard Mode

As was mentioned earlier, the "standard mode" involves synchronization between a Corus node and the Docker daemon running on its host. The diagram below provides hints as to the nature of the standard mode's interactions:

When a distribution is deployed to Corus, it is introspected to detect the presence of <docker> elements. If at least one such element is found, Corus automatically synchronizes the state of the local Docker daemon with the Docker registry (performing a "pull", in Docker's jargon). As illustrated in sample provided earlier, the <docker> element supports an image attribute (among others). This attribute indicates which Docker image to pull from the Docker registry at deployment time.

Conversely, upon undeployment of the distribution, the specified Docker image will be removed from the Docker daemon. This automatic removal can be disabled, as described in the relevant part of the Corus descriptor documentation. Auto-removal can also be configured at the Corus node level: in such a case. If auto-removal is enabled at the level of the Corus node, setting to true at the level of the process will override the node-level setting.

Therefore, Corus automatically performs upkeep of the local Docker daemon by synchronizing its own state with it, during deployment and undeployment.

Keep in mind: the image attribute is optional. If not specified, Corus will attempt pulling an image corresponding to the distribution's name and version according to the following format: ${name}:${version}.

When performing clustered deployment, the above behavior can be generalized to all hosts in the cluster, as shown below:

  1. The client initiates the deployment of a distribution.
  2. As the distribution is deployed to each node in the cluster, it is analyzed to detect the presence of Docker-related configuration, and the "pull" of the required Docker images is issued accordingly - by each Corus node.
The diagram above does not illustrate the Docker daemon on each host, for simplicity, but you should assume that each Corus node above resides with its Docker daemon, on its host, and that the pull is in fact delegated to the Docker daemon.

Registry-Less Mode

The "registry-less mode", for its part, is meant to minimize the dependency on the Docker registry: rather than each Corus node triggering the pull from the registry to its Docker daemon, the client saves the Docker image (from the registry) locally, and then deploys it to the cluster, which has the effect of performing a "load" of the image (in Docker's jargon) into each Docker daemon. This interaction is illustrated by the diagram below, from the point of view of a single Corus node (and corresponding Docker daemon):

From a macro-level, cluster-wide, the interactions in registry-less mode look as follows:

  1. The client saves locally the image(s) on which a given distribution depends.
  2. The client deploys the image(s) to the cluster (each Corus node will load the image(s) into its corresponding Docker daemon).
  3. The client then initiates the deployment of the distribution.
  4. As the distribution is deployed to each node in the cluster, it is analyzed to detect the presence of Docker-related configuration: if the corus.server.docker.registry.sync-enabled flag is set to false and the expected image(s) is/are not present, the deployment will result in an error - otherwise, Corus will trigger a pull of the missing image(s) from the Docker registry.

From the description above, one can observe that beyond the deployment of the Docker images, the ulterior steps are quite the same as in the standard mode.

For the registry-less mode to be enforced, you shoud set the corus.server.docker.registry.sync-enabled flag in corus.properties to false (it is set to true be default): if a required image for a distribution has not previously been deployed, Corus will then raise an exception, which should be the behavior one expects under the registry-less mode.

In order to allow for the explicity management of Docker images (more importantly, their deployment as suggested by the steps above), Corus commands are provided (both through the CLI and the REST API) to manage the lifecyle of Docker images (which only makes sense it the context of the registry-less mode). For example, to deploy an image:

deploy -docker -img mini/memached:latest -f memached.tar -cluster

To remove an image:

docker rm -n mini/memcached:* -cluster

Other commands, such as docker ls, docker ps, etc., are provided by the CLI. For more info, type man docker in the CLI.

Configuration

The behavior of the Corus/Docker integration can be altered through a set of properties, in the corus.properties file. The following table lists these properties and provides a description for each:

Name Description
corus.server.docker.enabled Indicates if the Corus/Docker integration should be enabled (defaults to false). Attempting to deploy Docker-enabled distributions when this flag is set to false will result in an error.
corus.server.docker.registry.sync-enabled This flag enables/disables synchronization with the Docker registry (defaults to true): if it is set to false, deploying Docker-enabled distributions will result in an error when Corus attempts to perform a pull/remove upon deployment/undeployment (as it would do in standard mode). Therefore, setting this flag to false makes sense in registry-less mode only (it should indeed be set to false in such a context).
corus.server.docker.image-auto-remove-enabled This flag enables/disables the auto-removal of images upon undeployment of the distributions that depend on these images. The flag is set to true by default. Setting it to false will have the effect of leaving the images in the Docker daemon (such images can be removed explicitely at a later stage, cluster-wide, either through the CLI or the REST API - as explained above in the section pertaining to the registry-less mode).
corus.server.docker.client.email The email address internally used for authentication, when connecting with the Docker registry.
corus.server.docker.client.username The username internally used for authentication, when connecting with the Docker registry.
corus.server.docker.client.password The password internally used for authentication, when connecting with the Docker registry.
corus.server.docker.registry.address The address of the Docker registry being used (defaults to https://hub.docker.com/v1/).
corus.server.docker.daemon.url The address of the Docker daemon (defaults to unix:///var/run/docker.sock).
corus.server.docker.certificates.path The path to the certificates required to connect to the Docker daemon, if TLS authentication is used. See Docker's documentation for what this amounts to.

Corus Descriptor Tips

This section provides a few Docker-related Corus descriptor tips. The full spec is available in the Corus Descriptor section of this guide.

Configuring Port Mappings

Corus' port management feature can be put to good use in the context of assigning host ports to container ports. The following demonstrates how this is done:

<distribution name="memcached" version="1.0"
  xmlns="http://www.sapia-oss.org/xsd/corus/distribution-5.0.xsd">

  <process name="server" maxKillRetry="3" shutdownTimeout="300000" invoke="true">

    <port name="memcached" />
    
    <docker image="mini/memcached" profile="default">
      <portMapping hostPort="${corus.process.port.memcached}" containerPort="11211" />
    </docker>

  </process>
</distribution>

Of course the above means that the corresponding port range has been pre-configured:

port add -n memcached -min 11210 -max 11215

Docker Container Health Check

In order to benefit from the Corus' high-availability support for application processes, the Docker containers started with Corus should have the proper diagnostic configuration defined.

Currently, the only support available is for HTTP/HTTPS endpoints (that is, processes are expected to expose a HTTP or HTTPS URL for pinging. The following provides an example illustrating how this would look like for Docker-based processes:

<distribution name="http-service" version="1.0"
  xmlns="http://www.sapia-oss.org/xsd/corus/distribution-5.0.xsd">

  <process name="server" maxKillRetry="3" shutdownTimeout="300000" invoke="true">

    <port name="http">
    	<http-diagnostic path="/system/ping" />
    </port>
    
    <docker image="mini/memcached" profile="default">
      <portMapping hostPort="${corus.process.port.http}" containerPort="80" />
    </docker>

  </process>
</distribution>

Volume Mappings

Docker supports mapping directories on the host to paths that have been configured at the Docker image level. Such directories and paths are called "volumes" in Docker's terminology. The notion of mapping a directory path at the image level to a directory on the host is dubbed "volume mapping". The Corus descriptor supports the configuration of such volume mappings (as most values in the descriptor, such mappings can be dynamically configured through process property variables):

<distribution name="postgres" version="1.0"
  xmlns="http://www.sapia-oss.org/xsd/corus/distribution-5.0.xsd">

  <process name="postgres" maxKillRetry="3" shutdownTimeout="300000" invoke="true">
    
    <docker image="my/postgres" profile="server">
      <volumeMapping hostVolume="${postgres.datadir}" containerVolume="/usr/local/pgsql/data" />
    </docker>

  </process>
</distribution>

In the above case, a command such as the following would be used to add the relevant process property to Corus:

conf add -p postgres.datadir=/opt/postgres/data

Environment Variables

Docker containers accept environment variables at startup. Again, Corus process properties come handy as they can be passed to containers as environment variables, as follows:

<distribution name="memcached" version="1.0"
  xmlns="http://www.sapia-oss.org/xsd/corus/distribution-5.0.xsd">

  <process name="server" maxKillRetry="3" shutdownTimeout="300000" invoke="true">

    <port name="memcached" />
    
    <docker image="mini/memcached" profile="default">
      <portMapping hostPort="${corus.process.port.memcached}" containerPort="11211" />
      <env>
        <property name="MEMCACHED_MEMORY" value="${memcached.mem.size}" />
      </env>
    </docker>

  </process>
</distribution>

The following command would add the corresponding process property to Corus:

conf add -p memcached.mem.size=1024

The <env> element supports multiple <property> elements. Each such property element will be passed as an environment variable to the container being started.

Controlling Container Execution through Corus Tags

Corus' tagging feature can seamlessly be used in the context of running Docker containers. Taking our sample corus.xml configuration above and enriching it with tagging will provide a concrete example:

<distribution name="memcached" version="1.0"
		xmlns="http://www.sapia-oss.org/xsd/corus/distribution-5.0.xsd">
	
  <process name="server" maxKillRetry="3" shutdownTimeout="300000" invoke="true" tags="memached-enabled">
	
    <port name="memcached" />
			
    <docker image="mini/memcached" profile="default">
      <portMapping hostPort="${corus.process.port.memcached}" containerPort="11211" />
    </docker>
	
  </process>
</distribution>

In the above, we've added the memcached-enabled tag (throug the tags attribute). This makes the execution of the corresponding process (and thus, Docker container) conditional to the tag specified at the process being matched, at runtime, by the tags set on the Corus instance.

Therefore, the command below would have to be executed on all nodes on which the given process would be intended to run:

conf add -t memcached-enabled