Docker
Trenissimo can execute and terminate Docker containers through Ant tasks. A use case could be executing an external dispatching algorithm that Trenissimo doesn't have in parallel with the simulation, or a reporting tool at the end of the simulation.
The Trenissimo docker client is compatible with Docker Engine API v1.40. Make sure that you've installed a compatible Docker or Desktop Docker.
This guide requires basic knowledge of Apache Ant and Docker.
Quick start
Launch a container that writes "Hello World" and exits before the simulation.
How it works
Tutorials
Container in execution during the simulation
The goal is to create and execute a container right before the simulation starts, wait till all the necessary services are in execution (before the simulation starts), and at the end of the simulation stop the container, and if necessary, execute, and delete it.
- Open the simulation's
build.xmlfile, from Trenissimo's Files panel. - Add the namespace declaration for
com.trenolab.trilogy.dockermodule in theprojectroot element, like so:<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:trilogy="antlib:com.trenolab.trilogy.ant"
xmlns:if="ant:if"
xmlns:unless="ant:unless"
xmlns:docker="antlib:com.trenolab.trilogy.docker"
basedir="."
default="build"
name="Simulation"> - Extend the target
pre-pre-run, this way you can execute the necessary tasks for the container's execution before the simulation starts.<target extensionOf="pre-pre-run" name="pre-pre-run-myapp">
...
</target> - Inside the
pre-pre-run-myapptarget that we've just created, we should firstly initialize the Docker client.- The default configuration is compatible with Docker for Linux, and Docker Desktop for macOS:
<target extensionOf="pre-pre-run" name="pre-pre-run-myapp">
<docker:configureClient/>
...
</target> - On Windows, the
dockerHostparameter must be specified.<target extensionOf="pre-pre-run" name="pre-pre-run-myapp">
<docker:configureClient dockerHost="npipe:////./pipe/docker_engine"/>
...
</target> - If a cross-platform configuration is required, use an Ant
conditionlike this:<target extensionOf="pre-pre-run" name="pre-pre-run-myapp">
<condition property="docker.host"
value="npipe:////./pipe/docker_engine"
else="unix:///var/run/docker.sock" >
<os family="windows" />
</condition>
<docker:configureClient dockerHost="${docker.host}"/>
...
</target>
- The default configuration is compatible with Docker for Linux, and Docker Desktop for macOS:
- After the client has been initialized, it is possible to execute the container. The following is a complete example (rather complex). We will later discuss in depth each part of the container's configuration.
<target extensionOf="pre-pre-run" name="pre-pre-run-myapp">
...
<docker:startContainer imageName="my-app:latest"
containerIdProperty="docker.container.id"
autoRemove="true">
<authConfig username="user"
password="P@ssw0rd"/>
<bind source="${params.outputDir}/my-app/"
destination="/var/my-app/logs"/>
<portBinding exposedPort="80/tcp"
publishedPortProperty="docker.port.tcp.80"/>
<portBinding exposedPort="8088/tcp"
hostPort="8088/tcp"/>
<redirector alwayslog="false"
output="${{params.outputDir}}/my-app-debug-log" />
</docker:startContainer>
...
</target>- It is essential to specify the name of the image to be used with the
imageNameparameter, using therepository[:tag]format. If the tag is omitted, the defaultlatestwill be used.<docker:startContainer imageName="my-app:latest"
containerIdProperty="docker.container.id"
autoRemove="true">
...
</docker:startContainer> - The
containerIdPropertyparameter is the property name to be set, it contains the container's id (hash). It will be possible to reference this container using this property in other tasks. - The
autoRemoveparameter, whentrue, will delete the container at the end of the execution. It is equivalent to the--rmoption in Docker client's CLI. - If the docker image has to be downloaded from a non-public registry, insert a
authConfigelement containing the username and password for the registry.<authConfig username="user"
password="P@ssw0rd"/> - The
bindelement creates a bind between the host's directory and the container's directory. This way, for example, the logs of the application are written to the output directory of the simulation, to archive them together with the other results.When possible, you should always bind the application's output directory inside the simulation's output directory, using the<bind source="${params.outputDir}/my-app/"
destination="/var/my-app/logs"/>params.outputDirproperty. Trenissimo will automatically delete output directory during the clean phase, to avoid the project's size growth with time. - If the services in execution must be exposed to the host (the machine on which Trenissimo is running), add a port binding. The
exposedPortparameter is mandatory, using theport[\proto]format. There are two ways of assigning the host port: either by specifying it in thehostPortparameter or by letting it be assigned automatically, in order to avoid possible conflicts. In the second case, thecontainerIdPropertyparameter is the name of the property to be set, containing the port number automatically assigned.<portBinding exposedPort="80/tcp"
publishedPortProperty="docker.port.tcp.80"/>
<portBinding exposedPort="8088/tcp"
hostPort="8088/tcp"/> - To redirect the container's standard output/error on a file, insert a
redirectorelement. Thealwayslogparameter enables the container's output on Trenissimo's standard output. It is recommended to set this parameter tofalseto avoid overlapping the simulation's log lines with the ones coming from the container. If possible, always write the output files inside the simulation's output directory, using theparams.outputDirproperty. Trenissimo will automatically delete output directory during the clean phase, to avoid the project's size growth with time.To learn more, check the Apache Ant manual<redirector alwayslog="false"
output="${params.outputDir}/my-app-debug-log" />
- It is essential to specify the name of the image to be used with the
- Now that the container has been created and Docker has taken charge of its startup, it is not guaranteed that it will start correctly, or that it will have the running status immediately. Thus we need to wait for the container to start correctly:
Note the
<target extensionOf="pre-pre-run" name="pre-pre-run-myapp">
...
<docker:startContainer containerIdProperty="docker.container.id">
...
</docker:startContainer>
<waitfor maxwait="15" maxwaitunit="second"
timeoutproperty="docker.container.timeout">
<docker:status containerId="${docker.container.id}" status="running"/>
</waitfor>
<fail if:set="docker.container.timeout">Container not started</fail>
...containerIdProperty's usage, which specifies the name of the property that needs to be set with the container's id, and thecontainerIdthat specifies the container's id. If the application executed in the container requires a startup time, it is possible to wait until it starts, for example searching for a specific message contained in the log. The following example is based on ActiveMQ, which is notorious for writing the lineConnector ws startedwhen the service is in execution and can accept connections:If the container does not start in the specified time interval, the simulation fails, through the<waitfor maxwait="15" maxwaitunit="second" checkevery="500"
timeoutproperty="docker.activemq.container.timeout">
<resourcecontains resource="${params.outputDir}/debug-activemq.log"
substring="Connector ws started"/>
</waitfor>
<fail if:set="docker.container.timeout">ActiveMQ not started</fail>failtask - At this point, the container is being executed and can accept inbound network connections, or access the simulation files.
- If at the end of the simulation you want to stop the container and delete it, extend the target
post-post-runand call thestopContainertask:<target extensionOf="post-post-run" name="post-post-run-myapp">
<docker:stopContainer containerId="${docker.container.id}"/>
</target> - Execute the simulation
In this guide we introduced the docker:configureClient, docker:startContainer, docker:stopContainer tasks, the docker:status condition and the data types bind, portBinding and authConfig. docker:startContainer, docker:stopContainer. You can learn more in this Reference
Networking between containers
The goal is to execute multiple containers and to allow communication between them and the host over the network. A use case is if you want to execute an ActiveMQ broker or other middleware in order to allow the simulator to communicate with a dispatching algorithm external to Trenissimo.
This guide requires that one of the two containers is correctly configured following the Tutorial
The integration between Trenissimo and Docker takes into account that Trenissimo may execute multiple simulations at the same time. It is often impossible, and also very discouraged to assign manually ip addresses or names to the containers, networks, volumes, etc. In order to reference these objects in the scripts, the docker client takes charge of assigning unique names where needed and makes them available to the scripts through Ant properties.
- To allow communication between containers, it is necessary to firstly create a user-defined bridge. The
networkIdPropertyparameter should be set to the created network's id (hash). TheautoRemoveparameter makes it so that this bridge is deleted when the simulation is over, avoiding the consumption of all the Docker's host available networks.<target extensionOf="pre-pre-run" name="pre-pre-run-myapp">
<docker:configureClient/>
<docker:createNetwork networkIdProperty="docker.network.id"
autoRemove="true"/>
...
</target> - Call a
startContainertask for each needed container, specifying thenetworkIdparameter. Make sure you use different names forcontainerIdPropertyin each container. - Chiamare un task
startContainerper ciascun container necessario, specificando il parametronetworkId. Fare attenzione a usare un nome diverso percontainerIdPropertyin ciascun container!<docker:startContainer imageName="my-app:latest"
containerIdProperty="docker.activemq.container.id"
networkId="${docker.network.id}">
... - User-defined bridges provide automatic DNS resolution between containers, by means of the container alias. Define a unique alias for each container used by the simulation.
<docker:startContainer imageName="activemq:latest"
containerIdProperty="docker.activemq.container.id"
networkId="${docker.network.id}">
<alias value="activemq"/>
</docker:startContainer>
<docker:startContainer imageName="my-app:latest"
containerIdProperty="docker.myapp.container.id"
networkId="${docker.network.id}">
<alias value="myapp"/>
</docker:startContainer> - If necessary, you can pass the alias of the first container to the second container through a command line argument or an environment variable:
<docker:startContainer>
<arg value="activemq"/>
</docker:startContainer><docker:startContainer>
<env key="ACTIVEMQ_HOST" value="activemq"/>
</docker:startContainer> - At the end of the simulation, stop both containers, following the reverse creation order.
<target extensionOf="post-post-run" name="post-post-run-myapp">
<docker:stopContainer containerId="${docker.myapp.container.id}"/>
<docker:stopContainer containerId="${docker.activemq.container.id}"/>
</target> - Execute the simulation