Amplify your testing with more configurations!
We describe here how to run CAMP on the CityGo use-case. In particular we explain:
The related files are available in the GitHub repository, under
samples/citygo
.
$ git clone https://github.com/STAMP-project/camp
$ cd samples/stamp/atos
We assume hereafter that you already have CAMP up and running. If not, please refer to installation instructions. Note that the CityGo applications, is not open source, so you won’t be able to actually deploys and run it. We can however generate and realize the configurations.
The CityGO application is a Python web application running on top of the Django framework. The CityGo case-studies comes into flavours: functionals and performance tests.
As explained above, in this scenario we run performance tests using
JMeter. The source files for this
scenario are available in the samples/stamp/atos/performance
directory.
The main objective of this case study is to modify numerical
variables, which governs the configuration of the Apache server. In
the CAMP model (i.e., the file camp.yml
), these variables are
attached to the “apache” component, although the substitution will
affect the Apache Server as defined in the docker-compose file.
The extract below show the apache
component and its three
variables, namely thread_limit
, threads_per_child
and
max_request_workers
.
[...]
apache:
provides_services: [ HttpProxy ]
requires_services: [ CityGo ]
variables:
thread_limit:
type: Integer
values: [ 64, 128 ]
realization:
- targets: [ docker-compose.yml ]
pattern: "ThreadLimit=64"
replacements: [ ThreadLimit=64, ThreadLimit=128 ]
threads_per_child:
type: Integer
values:
range: [0, 128]
coverage: 10
realization:
- targets: [ docker-compose.yml ]
pattern: ThreadsPerChild=25
replacements: ["ThreadsPerChild={value}"]
max_request_workers:
type: Integer
realization:
- targets: [ docker-compose.yml ]
pattern: MaxRequestWorkers=150
replacements: ["MaxRequestWorkers={value}"]
implementation:
docker:
file: apache/Dockerfile
[...]
As for other case-studies, you can generate all possible configurations with the following command:
$ camp generate -d . --mode all
In general, the presence of an unbound variable, such
max_request_workers
entails an infinte number of configurations
Here however, there are only 10 configurations.
There are three variables:
thread_limit
, which is either 64 or 128thread_per_child
, which ranges from 0 to 128, with a maximum
coverage of 10. The actual values are therefore [0, 8, 16, 32, 40,
48, 56, 64, 72, 80, 88, 96, 104, 112, 120, 128].max_request_worker
which is not bounded.The CAMP model set the three following constraints:
thread_limit
> thread_per_child
thread_per_child
* 16 = max_request_worker
thread_per_child
>= 100 and max_request_worker
>= 200) or
(thread_limit
<= 64 and max_request_worker
>= 2)From Constraint 1 and the second part of Constraint 3, we know that
thread_per_child
must be lower than thread_per_child, and therefore
strictly lower than 64. This reduces its possible values to the range
[0, 56]. From Constraint 2, max_request_worker
must be above 2, and
also equals 16 * thread_per_child
, which rules out the value 0,
leaving us with the range [8, 56].
From the first part of Constraint 1, we know that thread_per_child
must be above 100, which adds up the range [104, 112, 120, 128]. But
from Constraint 1, thread_per_child
must strictly below
thread_limit
, which invalidate value 128.
We are left with 10 possible values for thread_per_child
that are
[8, 16, 24, 32, 40, 48, 56, 104, 112, 120]. In addition, we need one
configuration to test every single value, so we need 10
configurations.
To generate the set of configurations that cover all variations, we use the following commands:
$ camp generate -d . --mode covering
CAMP v0.1.0 (MIT)
Copyright (C) 2017, 2018 SINTEF Digital
Model loaded from './camp.yml'.
Searching for configurations ...
- Config. 1 in './out/config_1/configuration.yml'.
Includes python, postgres, showcase (104, 1664, 128), ubuntu
- Config. 2 in './out/config_2/configuration.yml'.
Includes python, showcase (112, 1792, 128), postgres, ubuntu
...
- Config. 10 in './out/config_10/configuration.yml'.
Includes python, showcase (32, 512, 64), postgres, ubuntu
That's all folks!
CAMP found 10 configurations with various values for the three
variables, all placed in the out
directory:
$ tree out
out
├── config_1
│ ├── configuration.dot
│ └── configuration.yml
├── config_2
│ ...
To better visualise these configurations, we can generate PNG images using Graphviz and image magick as follows. The figure belows illustrates these selected configurations.
$ find . -name "*.dot" | xargs -I file dot -Tpng file -o file.png
$ find . -name "*.png" \
| tr '\n' ' ' \
| montage -label '%d/%f' @- -geometry 300x300 configurations.png
CAMP has generated abstract configurations in the out
folder. This means that CAMP
has so far only generated a YAML file for each configuration. This YAML file only indicates how
components are wired together and configured (i.e., variable
settings). Those abstract configurations cannot be directly executed or deployed. CAMP first needs to “realize” them i.e. to link
them to concrete, deployable artifacts (Docker configurations).
To transform abstract CAMP configurations into concrete Docker configurations which we can run,
we first need to fill the template directory with means to deploy
every component listed in the camp.yml
file.
Note CAMP and docker-compose both describes service orchestrations, but they work at different granularity levels. docker-compose only bind together services and assumes that each service is a Docker image. By contrast, CAMP works with components that it assembles into software stacks, each stacks becoming a service, running in a separate container, as in the docker-compose file.
So the template
directory must contains a docker-compose file, whose
services must point towards the “top” component of each software
stacks that CAMP generate.
So if we look at one specific abstract configuration that CAMP has generated, say Config 1 for instance, we see that Container 2 includes several components, that is citygo, running on top of python, running on top of ubuntu.
.
As usual, the template directory must provide installation material
for every component of the camp.yml
that is not implemented by a
pre-existing Docker image. We therefore omit both the hub
component
and the ubuntu
component.
We therefore create the following structure:
$ tree template -L 2
template
├── apache
│ ├── demo_site.conf
│ ├── Dockerfile
│ └── mpm_event.conf
├── browser
│ ├── Dockerfile
│ └── tests.py
├── citygo
│ └── Dockerfile
├── docker-compose.yml
├── postgres
│ ├── Dockerfile
│ ├── init-db.sql
│ └── postgresql.conf
└── python
└── Dockerfile
In Container 2, CAMP will reassemble the components by creating an
image where python is deployed on top of ubuntu, and then another
image where the citygo app is deployed on top of those two. To do so
we need Dockerfile with specific FROM
statements that CAMP can
override to assemble the component as it needs. For instance the
Dockerfile of the citygo component looks like:
FROM camp/runtime
# Describe here how to install Citygo only. CAMP will replace the
# 'FROM' statement so that it points to an image that already includes
# both Python and Ubuntu.
RUN apt-get updrade \
&& ...
CAMP searches for FROM statements whose image name starts with
camp/
. By convention, we use ‘camp/runtime’, to emphasize that the
FROM statement that CAMP will override at runtime.
Now, the services in the template ‘docker-compose’ must points towards single components in the template directory. When a service in the docker-compose file match a ‘stack’ assembled by CAMP, the docker-compose file must point towards the top component of the stack. For instance, in Container 2, the citygo service must point to the citygo component. For instance:
web:
build: ./citygo
container_name: "my_web"
restart: always
environment:
- DJANGO_SETTINGS_MODULE=citygo_settings.settings
- BROWSERNAME=chrome
Once the template directory contains all needed material to deploy all individual component, and the docker-compose file is consistent, we can invoke CAMP generate using the following command:
$ camp realize -d .
CAMP v0.6.3 (MIT)
Copyright (C) 2017 -- 2019 SINTEF Digital
Model loaded from './camp.yml'.
Searching configurations in './out' ...
- Building './out/config_5' ...
- Building './out/config_3' ...
- Building './out/config_7' ...
- Building './out/config_4' ...
- Building './out/config_1' ...
- Building './out/config_8' ...
- Building './out/config_6' ...
- Building './out/config_9' ...
- Building './out/config_10' ...
- Building './out/config_2' ...
That's all folks!
We can see that CAMP has generated modified the template for each configurations. For instance:
$ tree -L 3 out/config_1
out/config_1
├── configuration.dot
├── configuration.yml
├── docker-compose.yml
└── images
├── apache_0
│ ├── demo_site.conf
│ ├── Dockerfile
│ └── mpm_event.conf
├── browser_0
│ ├── Dockerfile
│ └── tests.py
├── build_images.sh
├── citygo_0
│ └── Dockerfile
├── postgres_0
│ ├── Dockerfile
│ ├── init-db.sql
│ └── postgresql.conf
└── python_0
└── Dockerfile
Note that CAMP has generated a specific shell script
build_images.sh
, which it will use to build the images needed for
Container 2.
#!/bin/bash
#
# Generated by CAMP. Edit carefully
#
# Build all images and set the appropriate tags
#
set -e
docker build --no-cache -t camp-python_0 ./python_0
docker build --no-cache -t camp-citygo_0 ./citygo_0
echo 'All images ready.'
Above, we see that this script will create images with specific tags
and that this tags are placed into the FROM
statement of the related
Dockerfiles. For instance, the Dockerfile of the citygo_0
component
contains:
FROM camp-python_0
# Describe here how to install Citygo only. CAMP will replace the
# 'FROM' statement so that it points to an image that already includes
# both Python and Ubuntu.
RUN apt-get updrade \
&& ...
In the functional tests scenario, we focus on injecting architectural changes. We replace Apache with NGinx, we switch between alternative versions of both the Django framework and PostgresSQL database. We options we select are:
camp.yml
To keep things simple, we model change of version using
variables. For instance, we add a variable named django
to the
citygo
component, as follows.
citygo:
provides_services: [ CityGo ]
requires_services: [ Postgres, Mongo ]
requires_features: [ Python ]
variables:
django:
values: [ v1.10.2, v2.2.6 ]
We proceed in the same way for Postgres and for the option of the NGinx proxy.
Switching between Apache or NGinx translates in both component having
the same signature: Both the apache
and the nginx
component provides
the HttpProxy
service and requires the CityGo
service.
nginx:
provides_services: [ HttpProxy ]
requires_services: [ CityGo ]
variables:
gzip:
values: [on, off]
implementation:
docker:
file: nginx/Dockerfile
apache:
provides_services: [ HttpProxy ]
requires_services: [ CityGo ]
implementation:
docker:
file: apache/Dockerfile
As shown in the figure below, camp generates 18 configurations using the followings command. We omit parts of the output for the sake of brevity.
$ camp generate -d . --mode all
CAMP v0.6.3 (MIT)
Copyright (C) 2017 -- 2019 SINTEF Digital
Loaded './camp.yml'.
- Config. 1 in './out/config_1/configuration.yml'.
Includes ubuntu, citygo (v1.10.2), apache, hub, postgres (v9), mongo, br...
[...]
- Config. 18 in './out/config_18/configuration.yml'.
Includes ubuntu, nginx (True), citygo (v2.2.6), hub, mongo, browser, pos...
That's all folks!
.
We may not be interested in testing every single configurations. Instead we may prefer to test every single options at least once. This finding a smaller number of configurations that use the two versions of Django, both apache and nginx, as well as the three version of Postgres.
CAMP can do this by computing a “covering array”, that is a small set of configuration where each option is used at least once. This yields 5 configurations. To do so, we proceed as follows
$ camp generate -d . --mode covering
CAMP v0.6.3 (MIT)
Copyright (C) 2017 -- 2019 SINTEF Digital
Loaded './camp.yml'.
- Config. 1 in './out/config_1/configuration.yml'.
Includes ubuntu, hub, python, postgres (v9), citygo (v1.10.2), browser, ...
- Config. 2 in './out/config_2/configuration.yml'.
Includes ubuntu, citygo (v2.2.6), hub, python, postgres (v10), browser, ...
- Config. 3 in './out/config_3/configuration.yml'.
Includes ubuntu, citygo (v2.2.6), hub, postgres (v11), python, browser, ...
- Config. 4 in './out/config_4/configuration.yml'.
Includes ubuntu, citygo (v2.2.6), hub, postgres (v11), python, browser, ...
- Config. 5 in './out/config_5/configuration.yml'.
Includes ubuntu, hub, python, citygo (v1.10.2), postgres (v10), browser,...
That's all folks!
.
At first CAMP generates only models of configurations (as YAML files). To obtain deployable configurations, we must complete the variation model and detail how each variation must be enacted.
The version of Postgres we use is setup in the associated
DockerFile,
more precisely, in the FROM
statement. We can switch version of
postgres by simply modifying this FROM statement. We can therefore
realise the variable we have created in the postgres
component
using a substitution, as follows:
postgres:
provides_services: [ Postgres ]
variables:
version:
values: [v9, v10, v11]
realization:
- targets: [ "postgres/Dockerfile" ]
pattern: "FROM postgres:9.3"
replacements:
- "FROM postgres:9.3"
- "FROM postgres:10"
- "FROM postgres:11"
Here, each value is associated with a different FROM
statement. This
realisation tells CAMP to search for the pattern FROM postgres:9.3
and to substitute it with the adequate replacement (replacements must
be ordered according to the variable’s values).
The Django framework comes as Python library, which is downloaded when
we install the CityGo application. The version of the Django framework
thus written down in the file requirements.txt
, which defines
the dependencies, as in the following example:
Django==1.10.2
django-allauth==0.27.0
django-rest-auth==0.8.1
djangorestframework==3.4.7
httplib2==0.9.2
oauth2==1.9.0.post1
oauthlib==2.0.0
psycopg2
psycopg2-binary
python-openid==2.2.5
requests>=2.18.2
requests-cache==0.4.12
We can therefore switch between versions of Django by replacing the
fragment Django==1.10.2
. We do so by defining the realisation of the
citygo
component as a substitution:
citygo:
provides_services: [ CityGo ]
requires_services: [ Postgres, Mongo ]
requires_features: [ Python ]
variables:
django:
values: [ v1.10.2, v2.2.6 ]
realization:
- targets: [ citygo/requirements.txt ]
pattern: "Django==1.10.2"
replacements:
- "Django==1.10.2"
- "Django==2.2.6"
To switch between Apache and NGinx requires to modify the service orchestration, and in turn, the docker-compose file. To do so, we define two docker-compose file and we select the correct one when a given component is selected.
apache:
provides_services: [ HttpProxy ]
requires_services: [ CityGo ]
implementation:
docker:
file: apache/Dockerfile
realization:
- select: docker-compose-apache.yml
instead_of:
- docker-compose-nginx.yml
as: docker-compose.yml
With this realisation, attached to the Apache component, when CAMP
realises a configuration that includes this component, it will select
the file named docker-compose-apache.yml
as the docker-compose.yml
and discards the other one.
We proceed in the same way for the NGinx component:
nginx:
provides_services: [ HttpProxy ]
requires_services: [ CityGo ]
variables:
gzip:
values: [on, off]
implementation:
docker:
file: nginx/Dockerfile
realization:
- select: docker-compose-nginx.yml
instead_of:
- docker-compose-apache.yml
as: docker-compose.yml
The GZip option is actually set as an environment variable passed to
associated container, through the docker-compose.yml
descriptor. We
can therefore simply search for the pattern gzip=on
and replace it
when needed, as follows:
nginx:
provides_services: [ HttpProxy ]
requires_services: [ CityGo ]
variables:
gzip:
values: [on, off]
realization:
- targets: [ docker-compose.yml ]
pattern: "gzip=on"
replacements:
- gzip=on
- gzip=off
implementation:
docker:
file: nginx/Dockerfile
Once all the possible option are realize, we can build the configurations as follows:
$ camp realize -d .
CAMP v0.6.3 (MIT)
Copyright (C) 2017 -- 2019 SINTEF Digital
Loaded './camp.yml'.
Loading configurations from './out' ...
- Built configuration './out/config_1.
- Built configuration './out/config_2.
- Built configuration './out/config_4.
- Built configuration './out/config_3.
- Built configuration './out/config_5.
That's all folks!