Ruan Bekker's Blog

From a Curious mind to Posts on Github

Setup a Concourse-CI Server on Ubuntu 16

Concourse is a Pipeline Based Continious Integration system written in Go

Resources:

What is Concourse CI:

Concourse CI is a Continious Integration Platform. Concourse enables you to construct pipelines with a yaml configuration that can consist out of 3 core concepts, tasks, resources, and jobs that compose them. For more information about this have a look at their docs

What will we be doing today

We will setup a Concourse Server on Ubuntu 16.04 and run the traditional Hello, World pipeline

Setup the Server:

Concourse needs PostgresSQL 9.3+

1
2
3
$ apt update && apt upgrade -y
$ apt install postgresql postgresql-contrib -y
$ systemctl enable postgresql

Create the Database and User for Concourse on Postgres:

1
2
$ sudo -u postgres createuser concourse
$ sudo -u postgres createdb --owner=concourse atc

Download the Concourse and Fly Cli Binaries:

1
2
3
4
5
$ wget https://github.com/concourse/concourse/releases/download/v3.6.0/concourse_linux_amd64
$ wget https://github.com/concourse/concourse/releases/download/v3.6.0/fly_linux_amd64
$ chmod +x concourse_linux_amd64 fly_linux_amd64
$ mv concourse_linux_amd64 /usr/bin/concourse
$ mv fly_linux_amd64 /usr/bin/fly

Create the Encryption Keys:

1
2
3
4
5
$ mkdir /etc/concourse
$ ssh-keygen -t rsa -q -N '' -f /etc/concourse/tsa_host_key
$ ssh-keygen -t rsa -q -N '' -f /etc/concourse/worker_key
$ ssh-keygen -t rsa -q -N '' -f /etc/concourse/session_signing_key
$ cp /etc/concourse/worker_key.pub /etc/concourse/authorized_worker_keys

Concourse Web Process Configuration:

1
2
3
4
5
6
7
8
9
10
$ cat /etc/concourse/web_environment

CONCOURSE_SESSION_SIGNING_KEY=/etc/concourse/session_signing_key
CONCOURSE_TSA_HOST_KEY=/etc/concourse/tsa_host_key
CONCOURSE_TSA_AUTHORIZED_KEYS=/etc/concourse/authorized_worker_keys
CONCOURSE_POSTGRES_SOCKET=/var/run/postgresql

CONCOURSE_BASIC_AUTH_USERNAME=admin
CONCOURSE_BASIC_AUTH_PASSWORD=secret
CONCOURSE_EXTERNAL_URL=http://10.20.30.40:8080

Concourse Worker Process Configuration:

1
2
3
4
5
6
$ cat /etc/concourse/worker_environment

CONCOURSE_WORK_DIR=/var/lib/concourse
CONCOURSE_TSA_WORKER_PRIVATE_KEY=/etc/concourse/worker_key
CONCOURSE_TSA_PUBLIC_KEY=/etc/concourse/tsa_host_key.pub
CONCOURSE_TSA_HOST=127.0.0.1

Create a Concourse user:

1
2
3
$ sudo adduser --system --group concourse
$ sudo chown -R concourse:concourse /etc/concourse
$ sudo chmod 600 /etc/concourse/*_environment

Create SystemD Unit Files, first for the Web Service:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ cat /etc/systemd/system/concourse-web.service

[Unit]
Description=Concourse CI web process (ATC and TSA)
After=postgresql.service

[Service]
User=concourse
Restart=on-failure
EnvironmentFile=/etc/concourse/web_environment
ExecStart=/usr/bin/concourse web

[Install]
WantedBy=multi-user.target

Then the SystemD Unit File for the Worker Service:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ cat /etc/systemd/system/concourse-worker.service

[Unit]
Description=Concourse CI worker process
After=concourse-web.service

[Service]
User=root
Restart=on-failure
EnvironmentFile=/etc/concourse/worker_environment
ExecStart=/usr/bin/concourse worker

[Install]
WantedBy=multi-user.target

Start and Enable the Services:

1
2
3
4
5
6
7
$ systemctl start concourse-web concourse-worker
$ systemctl enable concourse-web concourse-worker
$ systemctl status concourse-web concourse-worker

$ systemctl is-active concourse-worker concourse-web
active
active

The listening ports should more or less look like the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ netstat -tulpn

Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 127.0.0.1:7777          0.0.0.0:*               LISTEN      4530/concourse
tcp        0      0 127.0.0.1:7788          0.0.0.0:*               LISTEN      4530/concourse
tcp        0      0 127.0.0.1:8079          0.0.0.0:*               LISTEN      4525/concourse
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      1283/sshd
tcp        0      0 127.0.0.1:5432          0.0.0.0:*               LISTEN      4047/postgres
tcp6       0      0 :::36159                :::*                    LISTEN      4525/concourse
tcp6       0      0 :::46829                :::*                    LISTEN      4525/concourse
tcp6       0      0 :::2222                 :::*                    LISTEN      4525/concourse
tcp6       0      0 :::8080                 :::*                    LISTEN      4525/concourse
tcp6       0      0 :::22                   :::*                    LISTEN      1283/sshd
udp        0      0 0.0.0.0:68              0.0.0.0:*                           918/dhclient
udp        0      0 0.0.0.0:42165           0.0.0.0:*                           4530/concourse

Client Side:

I will be using a the Fly cli from a Mac, so first we need to download the fly-cli for Mac:

1
2
3
$ wget https://github.com/concourse/concourse/releases/download/v3.6.0/fly_darwin_amd64
$ chmod +x fly_darwin_amd64
$ alias fly='./fly_darwin_amd64'

Next, we need to setup our Concourse Target by Authenticating against our Concourse Endpoint, lets setup our target with the name ci:

1
2
3
4
5
6
7
$ fly -t ci login -c http://10.20.30.40:8080
logging in to team 'main'

username: admin
password:

target saved

Lets list our targets:

1
2
3
$ fly targets
name  url                        team  expiry
ci    http://10.20.30.40:8080    main  Wed, 08 Nov 2017 15:32:59 UTC

Listing Registered Workers:

1
2
3
$ fly -t ci workers
name              containers  platform  tags  team  state    version
ip-172-31-12-134  0           linux     none  none  running  1.2

Listing Active Containers:

1
2
$ fly -t ci containers
handle                                worker            pipeline     job            build #  build id  type   name                  attempt

Hello World Pipeline:

Let’s create a basic pipeline, that will print out Hello, World!:

Our hello-world.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
jobs:
- name: my-job
  plan:
  - task: say-hello
    config:
      platform: linux
      image_resource:
        type: docker-image
        source:
          repository: alpine
          tag: edge
      run:
        path: /bin/sh
        args:
        - -c
        - |
          echo "============="
          echo "Hello, World!"
          echo "============="

Applying the configuration to our pipeline:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
$ fly -t ci set-pipeline -p yeeehaa -c hello-world.yml
jobs:
  job my-job has been added:
    name: my-job
    plan:
    - task: say-hello
      config:
        platform: linux
        image_resource:
          type: docker-image
          source:
            repository: alpine
            tag: edge
        run:
          path: /bin/sh
          args:
          - -c
          - |
            echo "============="
            echo "Hello, World!"
            echo "============="

apply configuration? [yN]: y
pipeline created!
you can view your pipeline here: http://10.20.30.40:8080/teams/main/pipelines/yeeehaa

the pipeline is currently paused. to unpause, either:
  - run the unpause-pipeline command
  - click play next to the pipeline in the web ui

We can browse to the WebUI to unpause the pipeline, but since I like to do everything on cli as far as possible, I will unpause the pipeline via cli:

1
2
$ fly -t ci unpause-pipeline -p yeeehaa
unpaused 'yeeehaa'

Now our Pipeline is unpaused, but since we did not specify any triggers, we need to manually trigger the pipeline to run, you can either via the WebUI, select your pipeline which in this case will be named yeeehaa and then select the job, which will be my-job then hit the + sign, which will trigger the pipeline.

I will be using the cli:

1
2
$ fly -t ci trigger-job --job yeeehaa/my-job
started yeeehaa/my-job #1

Via the WebUI on http://10.20.30.40:8080/teams/main/pipelines/yeeehaa/jobs/my-job/builds/1 you should see the Hello, World! output, or via the cli, we also have the option to see the output, so let’s trigger it again, but this time passing the --watch flag:

1
2
3
4
5
6
7
8
9
10
11
12
$ fly -t ci trigger-job --job yeeehaa/my-job --watch
started yeeehaa/my-job #2

initializing
running /bin/sh -c echo "============="
echo "Hello, World!"
echo "============="

=============
Hello, World!
=============
succeeded

Listing our Workers and Containers again:

1
2
3
4
5
6
7
$ fly -t ci workers
name              containers  platform  tags  team  state    version
ip-172-31-12-134  2           linux     none  none  running  1.2

$ fly -t ci containers
handle                                worker            pipeline     job         build #  build id  type   name           attempt
36982955-54fd-4c1b-57b8-216486c58db8  ip-172-31-12-134  yeeehaa      my-job      2        729       task   say-hello      n/a

Installing Elastalert for Elasticsearch on Amazon Linux

Elastalert, a service for Alerting with Elasticsearch:

Setting up Elastalert

We will setup Elastalert for Elasticsearch on Amazon Linux which is a RHEL Based Distribution.

Setting up dependencies

1
2
3
4
5
6
7
8
9
10
$ sudo su
# yum update -y
# yum install git python-devel lib-devel libevent-devel bzip2-devel openssl-devel ncurses-devel zlib zlib-devel xz-devel gcc -y
# yum install python-setuptools -y
# easy_install pip
# pip install virtualenv
# virtualenv .venv
# source .venv/bin/activate
# pip install pip --upgrade
# pip install setuptools --upgrade

Clone Elastalert Repository and Install Dependencies:

1
2
3
$ git clone https://github.com/Yelp/elastalert
$ cd elastalert/
$ pip install -r requirements.txt

Configs:

1
2
3
$ cp config.yaml.example config.yaml
$ vim config.yaml
$ vim example_rules/example_frequency.yaml

After opening the config, populate the configuration where needed.

Installation of elastalert:

1
2
$ python setup.py install
$ elastalert-create-index

Running elastalert:

1
2
$ python -m elastalert.elastalert --verbose --rule example_frequency.yaml
INFO:elastalert:Starting up

Linux Shell Commands With the Python Commands Module

Using Python to Execute Shell Commands in Linux

Status Code and Output:

Getting the Status Code and the Output:

1
2
3
4
5
6
7
8
9
>>> import commands
>>> commands.getstatusoutput('echo foo')
(0, 'foo')

>>> status, output = commands.getstatusoutput('echo foo')
>>> print(status)
0
>>> print(output)
foo

Command Output Only:

Only getting the Shell Output:

1
2
3
>>> import commands
>>> commands.getoutput('echo foo')
'foo'

Basic Script

Test file with a one line of data:

1
2
$ cat file.txt
test-string

Our very basic python script:

1
2
3
4
5
6
7
import commands

status = None
output = None

status, output = commands.getstatusoutput('cat file.txt')
print("Status: {}, Output: {}".format(status, output))

Running the script:

1
2
$ python script.py
Status: 0, Output: test-string

Using Python to Query MySQL Database With MySQLdb Library

a Quick post to demostrate how to use Python to Query data from MySQL. We will use the MySQL Docker Image for the demonstration.

Provision MySQL

We will use the latest mysql image, and use the environment variable to pass the root password, and also expose the mysql port:

1
$ docker run -itd -p 3306:3306 -e MYSQL_ROOT_PASSWORD=password mysql

Populate some data in MySQL

Connect to MySQL:

1
$ mysql -h 127.0.0.1 -u root -ppasword

Create some test data:

1
2
3
4
5
6
mysql> create database foo;
mysql> use foo;
mysql> create table bar (name VARCHAR(20), surname VARCHAR(20));
mysql> insert into bar values('ruan', 'bekker');
mysql> insert into bar values('stefan', 'bester');
mysql> insert into bar values('peter', 'williams');

Python with MySQL: Setup the Environment

We will use virtualenv to create a virtual environment to keep our installation isolated from the rest of our system. Install virtualenv:

1
$ pip install virtualenv

Create a virtual environment and install the required dependency:

1
2
3
$ virtualenv venv-mysql
$ source venv-mysql/bin/activate
(venv-mysql) pip install MySQL-python

Python with MySQL: Develop the Client

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> import MySQLdb
>>> db = MySQLdb.connect('127.0.0.1', 'root', 'password', 'foo')
>>> con = db.cursor()
>>> con.execute("SELECT * from bar")
4L
>>> rows = con.fetchall()
>>> for row in rows:
...     print(row[0], row[1])
...
('ruan', 'bekker')
('stefan', 'bester')
('peter', 'williams')
>>> exit()

Your First Hello World App With Golang

So everyone has been saying how awesome Golang is, and at this moment, I am quite curious to fiddle with it.

Golang Environment: Golang Docker Image

A quick way to get a Golang Environment, will be to use Docker. We will be using the Alpine tag:

1
$ docker run -it golang:alpine sh

Our Basic App

After we are in our container, lets write our first Hello World App:

app.go
1
2
3
4
5
6
7
package main

import "fmt"

func main() {
  fmt.Println("Hello, World!")
}

Running our App:

Using golang to run our app:

1
2
$ go run app.go
Hello, World!

We can also build our app to create a executable binary:

1
$ go build app.go

You will find that there is a executable binary named app placed in the current working directory. Let’s execute it:

1
2
$ ./app
Hello, World!

This was a very basic example, but will add more examples as I am learning the language

Resources:

New Posts on Github Pages With Octopress Not Showing on Your Blog

So today I had the issue where new posts that was generated and pushed to github, not being displayed on my blog.

I was able to see the markdown pages on my github respository, but via the blog itself, getting 404’s.

The Issue:

When I did a rake generate I found the following error:

1
jekyll 2.5.3 | Error: invalid byte sequence in US-ASCII

Resolving the Issue:

After running the following, I was able to get rid of the error, and posts showing again:

1
2
$ export LC_ALL="en_US.UTF-8"
$ export LANG="en_US.UTF-8"

Running Java Web Applications on Tomcat With Docker Swarm

From this post we used Payara Micro to Setup a Web Application, and a full example was provided on how to create a war file that will be used for the deployment.

Today we will be using Tomcat to deploy the same application. The official repository can be found on hub.docker.com/_/tomcat .

Our Dockerfile for our Own Tomcat Image:

The Dockerfile is modified a bit (CATALINA_OPTS) to be able to pass JVM environment variables, but if you would like to use the standard image you can skip this and just use the image from their repository.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
FROM openjdk:8-jre-alpine

ENV CATALINA_HOME /usr/local/tomcat
ENV PATH $CATALINA_HOME/bin:$PATH
RUN mkdir -p "$CATALINA_HOME"
WORKDIR $CATALINA_HOME
ENV CATALINA_OPTS -Xmx768m -Xms512m -XX:PermSize=256m -XX:MaxPermSize=512m -XX:ReservedCodeCacheSize=64m -XX:+UseG1GC -XX:+CMSClassUnloadingEnabled -XX:+PrintHeapAtGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps
# let "Tomcat Native" live somewhere isolated
ENV TOMCAT_NATIVE_LIBDIR $CATALINA_HOME/native-jni-lib
ENV LD_LIBRARY_PATH ${LD_LIBRARY_PATH:+$LD_LIBRARY_PATH:}$TOMCAT_NATIVE_LIBDIR

RUN apk add --no-cache gnupg

# see https://www.apache.org/dist/tomcat/tomcat-$TOMCAT_MAJOR/KEYS
# see also "update.sh" (https://github.com/docker-library/tomcat/blob/master/update.sh)
ENV GPG_KEYS 05AB33110949707C93A279E3D3EFE6B686867BA6 07E48665A34DCAFAE522E5E6266191C37C037D42 47309207D818FFD8DCD3F83F1931D684307A10A5 541FBE7D8F78B25E055DDEE13C370389288584E7 61B832AC2F1C5A90F0F9B00A1C506407564C17A3 713DA88BE50911535FE716F5208B0AB1D63011C7 79F7026C690BAA50B92CD8B66A3AD3F4F22C4FED 9BA44C2621385CB966EBA586F72C284D731FABEE A27677289986DB50844682F8ACB77FC2E86E29AC A9C5DF4D22E99998D9875A5110C01C5A2F6059E7 DCFD35E0BF8CA7344752DE8B6FB21E8933C60243 F3A04C595DB5B6A5F1ECA43E3B7BBB100D811BBE F7DA48BB64BCB84ECBA7EE6935CD23C10D498E23
RUN set -ex; \
  for key in $GPG_KEYS; do \
      gpg --keyserver ha.pool.sks-keyservers.net --recv-keys "$key"; \
  done

ENV TOMCAT_MAJOR 8
ENV TOMCAT_VERSION 8.5.23
ENV TOMCAT_SHA1 1ba27c1bb86ab9c8404e98068800f90bd662523c

ENV TOMCAT_TGZ_URLS \
# https://issues.apache.org/jira/browse/INFRA-8753?focusedCommentId=14735394#comment-14735394
  https://www.apache.org/dyn/closer.cgi?action=download&filename=tomcat/tomcat-$TOMCAT_MAJOR/v$TOMCAT_VERSION/bin/apache-tomcat-$TOMCAT_VERSION.tar.gz \
# if the version is outdated, we might have to pull from the dist/archive :/
  https://www-us.apache.org/dist/tomcat/tomcat-$TOMCAT_MAJOR/v$TOMCAT_VERSION/bin/apache-tomcat-$TOMCAT_VERSION.tar.gz \
  https://www.apache.org/dist/tomcat/tomcat-$TOMCAT_MAJOR/v$TOMCAT_VERSION/bin/apache-tomcat-$TOMCAT_VERSION.tar.gz \
  https://archive.apache.org/dist/tomcat/tomcat-$TOMCAT_MAJOR/v$TOMCAT_VERSION/bin/apache-tomcat-$TOMCAT_VERSION.tar.gz

ENV TOMCAT_ASC_URLS \
  https://www.apache.org/dyn/closer.cgi?action=download&filename=tomcat/tomcat-$TOMCAT_MAJOR/v$TOMCAT_VERSION/bin/apache-tomcat-$TOMCAT_VERSION.tar.gz.asc \
# not all the mirrors actually carry the .asc files :'(
  https://www-us.apache.org/dist/tomcat/tomcat-$TOMCAT_MAJOR/v$TOMCAT_VERSION/bin/apache-tomcat-$TOMCAT_VERSION.tar.gz.asc \
  https://www.apache.org/dist/tomcat/tomcat-$TOMCAT_MAJOR/v$TOMCAT_VERSION/bin/apache-tomcat-$TOMCAT_VERSION.tar.gz.asc \
  https://archive.apache.org/dist/tomcat/tomcat-$TOMCAT_MAJOR/v$TOMCAT_VERSION/bin/apache-tomcat-$TOMCAT_VERSION.tar.gz.asc

RUN set -eux; \
  \
  apk add --no-cache --virtual .fetch-deps \
      ca-certificates \
      openssl \
  ; \
  \
  success=; \
  for url in $TOMCAT_TGZ_URLS; do \
      if wget -O tomcat.tar.gz "$url"; then \
          success=1; \
          break; \
      fi; \
  done; \
  [ -n "$success" ]; \
  \
  echo "$TOMCAT_SHA1 *tomcat.tar.gz" | sha1sum -c -; \
  \
  success=; \
  for url in $TOMCAT_ASC_URLS; do \
      if wget -O tomcat.tar.gz.asc "$url"; then \
          success=1; \
          break; \
      fi; \
  done; \
  [ -n "$success" ]; \
  \
  gpg --batch --verify tomcat.tar.gz.asc tomcat.tar.gz; \
  tar -xvf tomcat.tar.gz --strip-components=1; \
  rm bin/*.bat; \
  rm tomcat.tar.gz*; \
  \
  nativeBuildDir="$(mktemp -d)"; \
  tar -xvf bin/tomcat-native.tar.gz -C "$nativeBuildDir" --strip-components=1; \
  apk add --no-cache --virtual .native-build-deps \
      apr-dev \
      coreutils \
      dpkg-dev dpkg \
      gcc \
      libc-dev \
      make \
      "openjdk${JAVA_VERSION%%[-~bu]*}"="$JAVA_ALPINE_VERSION" \
      openssl-dev \
  ; \
  ( \
      export CATALINA_HOME="$PWD"; \
      cd "$nativeBuildDir/native"; \
      gnuArch="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)"; \
      ./configure \
          --build="$gnuArch" \
          --libdir="$TOMCAT_NATIVE_LIBDIR" \
          --prefix="$CATALINA_HOME" \
          --with-apr="$(which apr-1-config)" \
          --with-java-home="$(docker-java-home)" \
          --with-ssl=yes; \
      make -j "$(nproc)"; \
      make install; \
  ); \
  runDeps="$( \
     scanelf --needed --nobanner --format '%n#p' --recursive "$TOMCAT_NATIVE_LIBDIR" \
         | tr ',' '\n' \
         | sort -u \
         | awk 'system("[ -e /usr/local/lib/" $1 " ]") == 0 { next } { print "so:" $1 }' \
 )"; \
  apk add --virtual .tomcat-native-rundeps $runDeps; \
  apk del .fetch-deps .native-build-deps; \
  rm -rf "$nativeBuildDir"; \
  rm bin/tomcat-native.tar.gz; \
  \
# sh removes env vars it doesn't support (ones with periods)
# https://github.com/docker-library/tomcat/issues/77
  apk add --no-cache bash; \
  find ./bin/ -name '*.sh' -exec sed -ri 's|^#!/bin/sh$|#!/usr/bin/env bash|' '{}' +

# verify Tomcat Native is working properly
RUN set -e \
  && nativeLines="$(catalina.sh configtest 2>&1)" \
  && nativeLines="$(echo "$nativeLines" | grep 'Apache Tomcat Native')" \
  && nativeLines="$(echo "$nativeLines" | sort -u)" \
  && if ! echo "$nativeLines" | grep 'INFO: Loaded APR based Apache Tomcat Native library' >&2; then \
      echo >&2 "$nativeLines"; \
      exit 1; \
  fi

EXPOSE 8080
CMD ["catalina.sh", "run"]

Building our Image and Pusing to our Registry:

1
2
$ docker build -t registry.gitlab.com/<user>/<repo>/<image>:<tag>
$ docker push registry.gitlab.com/<user>/<repo>/<image>:<tag>

Dockerfile for our Application:

Now that we have built our image for Tomcat, we can write our Dockerfile for our application, note that the hello.war file also needs to be in the same working directory, unless written otherwise:

1
2
FROM registry.gitlab.com/<user>/<repo>/<image>:<tag>
COPY hello.war /usr/local/tomcat/webapps/hello.war

Setup the Compose file for our Stack:

We will use docker stack to deploy our application, note that I have Traefik that acts as my reverse proxy.

Below, our app.yml compose file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
version: '3'

services:
  hello:
    image: registry.gitlab.com/<user>/<repo>/<image>:<tag>
    networks:
      - appnet
    deploy:
      labels:
        - "traefik.port=8080"
        - "traefik.docker.network=appnet"
        - "traefik.frontend.rule=Host:apps.mydomain.com; PathPrefix: /hello/"
      mode: replicated
      replicas: 1
      placement:
        constraints:
          - 'node.role==worker'

networks:
  appnet:
    external: true

Deploy our Application:

From our compose file we defined that our network is external, so if you are using the same name, and you have not yet setup the overlay network:

1
$ docker network create --driver overlay appnet

Now deploy the stack:

1
$ docker stack deploy --compose-file app.yml apps

Testing our Application:

1
2
3
4
5
6
7
8
9
$ curl http://apps.mydomain.com/hello/

<!DOCTYPE html>
<html>
            Hello World!
   Test Page with Docker + Payara Micro</h3>

   Serving From ContainerId: d24f8cd982fc
</html>

Managing Traefik Configuration With Consul on Docker Swarm

Today we will Setup Consul with Traefik on Docker Swarm

Resources:

Create Consul in the Swarm:

Investigate using Consul with Traefik in Docker Swarm.

I have configured consul with constraints to be placed only on my one manager, as I was bind mounting the data directory to /mnt, as this was for testing on a small docker swarm cluster, but with Docker for AWS, we will use cloudstor with EFS, or GlusterFS, NFS for data persistency across any nodes.

Create Compose Files:

  • consul.yml
  • traefik.yml
  • apps.yml

consul.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$ cat > consul.yml << EOF
version: '3.3'

services:
  consul:
    image: progrium/consul
    command: -server -bootstrap -log-level debug -ui-dir /ui
    networks:
      - appnet
    deploy:
      placement:
        constraints: [node.role == manager]
    volumes:
      - type: bind
        source: /mnt/consul
        target: /data
    ports:
      - "8400:8400"
      - "8500:8500"
      - "8600:53/udp"

networks:
  appnet:
    external: true

EOF

traefik.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ cat > traefik.yml << EOF
version: '3.3'

services:
  traefik:
    image: traefik
    networks:
      - appnet
    command: --consul --consul.endpoint=consul:8500
    ports:
      - "80:80"
      - "8080:8080"

networks:
  appnet:
    external: true

EOF

apps.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
$ cat > apps.yml << EOF
version: '3.3'

services:
  whoami1:
    image: emilevauge/whoami
    networks:
      - appnet

  whoami2:
    image: emilevauge/whoami
    networks:
      - appnet

  whoami3:
    image: emilevauge/whoami
    networks:
      - appnet

  whoami4:
    image: emilevauge/whoami
    networks:
      - appnet

  whoami5:
    image: rbekker87/flask-containername
    networks:
      - appnet

  whoami6:
    image: rbekker87/flask-containername
    networks:
      - appnet

networks:
  appnet:
    external: true

EOF

Create Overlay Network and Deploy Stacks

Create the overlay network, and deploy the 3 stacks:

1
2
3
4
$ docker network create --driver=overlay appnet
$ docker stack deploy --compose-file consul.yml kvstore
$ docker stack deploy --compose-file traefik.yml proxy
$ docker stack deploy --compose-file apps.yml apps

Populate Configs and Push to Consul KV Store:

Config for Traefik:

1
2
3
4
5
6
7
8
9
$ cat > create_traefik_config.sh << EOF
curl -XPUT http://127.0.0.1:8500/v1/kv/traefik/loglevel -d 'DEBUG'
curl -XPUT http://127.0.0.1:8500/v1/kv/traefik/defaultentrypoints/0 -d 'http'
curl -XPUT http://127.0.0.1:8500/v1/kv/traefik/entrypoints/http/address -d ':80'
curl -XPUT http://127.0.0.1:8500/v1/kv/traefik/consul/endpoint -d 'consul:8500'
curl -XPUT http://127.0.0.1:8500/v1/kv/traefik/consul/watch -d 'true'
curl -XPUT http://127.0.0.1:8500/v1/kv/traefik/consul/prefix -d 'traefik'
curl -XPUT http://127.0.0.1:8500/v1/kv/traefik/web/address -d ':8081'
EOF

Config for WhoAmI Web Apps:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
$ cat > create_whoami_config.sh << EOF
# backend-1
curl -XPUT http://127.0.0.1:8500/v1/kv/traefik/backends/backend1/circuitbreaker/expression -d 'NetworkErrorRatio() > 0.5'
curl -XPUT http://127.0.0.1:8500/v1/kv/traefik/backends/backend1/servers/server1/url -d 'http://whoami1:80'
curl -XPUT http://127.0.0.1:8500/v1/kv/traefik/backends/backend1/servers/server1/weight -d '10'
curl -XPUT http://127.0.0.1:8500/v1/kv/traefik/backends/backend1/servers/server2/url -d 'http://whoami2:80'
curl -XPUT http://127.0.0.1:8500/v1/kv/traefik/backends/backend1/servers/server2/weight -d '1'
curl -XPUT http://127.0.0.1:8500/v1/kv/traefik/backends/backend1/servers/server2/tags -d 'api,helloworld'

# backend-2
curl -XPUT http://127.0.0.1:8500/v1/kv/traefik/backends/backend2/maxconn/amount -d '10'
curl -XPUT http://127.0.0.1:8500/v1/kv/traefik/backends/backend2/maxconn/extractorfunc -d 'request.host'
curl -XPUT http://127.0.0.1:8500/v1/kv/traefik/backends/backend2/loadbalancer/method -d 'drr'
curl -XPUT http://127.0.0.1:8500/v1/kv/traefik/backends/backend2/servers/server1/url -d 'http://whoami3:80'
curl -XPUT http://127.0.0.1:8500/v1/kv/traefik/backends/backend2/servers/server1/weight -d '1'
curl -XPUT http://127.0.0.1:8500/v1/kv/traefik/backends/backend2/servers/server2/url -d 'http://whoami4:80'
curl -XPUT http://127.0.0.1:8500/v1/kv/traefik/backends/backend2/servers/server2/weight -d '2'
curl -XPUT http://127.0.0.1:8500/v1/kv/traefik/backends/backend2/servers/server2/tags -d 'web'

# frontend-1
curl -XPUT http://127.0.0.1:8500/v1/kv/traefik/frontends/frontend1/backend -d 'backend2'
curl -XPUT http://127.0.0.1:8500/v1/kv/traefik/frontends/frontend1/routes/test_1/rule -d 'Host:test.localhost'

# frontend-2
curl -XPUT http://127.0.0.1:8500/v1/kv/traefik/frontends/frontend2/backend -d 'backend1'
curl -XPUT http://127.0.0.1:8500/v1/kv/traefik/frontends/frontend2/passHostHeader -d 'true'
curl -XPUT http://127.0.0.1:8500/v1/kv/traefik/frontends/frontend2/priority -d '10'
curl -XPUT http://127.0.0.1:8500/v1/kv/traefik/frontends/frontend2/entrypoints -d 'http'
curl -XPUT http://127.0.0.1:8500/v1/kv/traefik/frontends/frontend2/routes/test_2/rule -d 'PathPrefix:/test'
EOF

Config for Flask Container Name Web Apps:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ cat > create_flask_config.sh << EOF
# backends
curl -XPUT http://127.0.0.1:8500/v1/kv/traefik/backends/backend3/amount -d '5'
curl -XPUT http://127.0.0.1:8500/v1/kv/traefik/backends/backend3/maxconn/extractorfunc -d 'request.host'
curl -XPUT http://127.0.0.1:8500/v1/kv/traefik/backends/backend3/loadbalancer/method -d 'drr'

curl -XPUT http://127.0.0.1:8500/v1/kv/traefik/backends/backend3/servers/server1/url -d 'http://whoami5:5000'
curl -XPUT http://127.0.0.1:8500/v1/kv/traefik/backends/backend3/servers/server1/weight -d '1'
curl -XPUT http://127.0.0.1:8500/v1/kv/traefik/backends/backend3/servers/server1/tags -d 'flask'

curl -XPUT http://127.0.0.1:8500/v1/kv/traefik/backends/backend3/servers/server2/url -d 'http://whoami6:5000'
curl -XPUT http://127.0.0.1:8500/v1/kv/traefik/backends/backend3/servers/server2/weight -d '2'
curl -XPUT http://127.0.0.1:8500/v1/kv/traefik/backends/backend3/servers/server2/tags -d 'flask'

# frontend:
curl -XPUT http://127.0.0.1:8500/v1/kv/traefik/frontends/frontend3/backend -d 'backend3'
curl -XPUT http://127.0.0.1:8500/v1/kv/traefik/frontends/frontend3/routes/test_1/rule -d 'Host:flask.localhost'
EOF

Push Configs to Consul:

1
2
3
$ sh create_traefik_config.sh
$ sh create_whoami_config.sh
$ sh create_flask_config.sh

Testing Applications:

Test Frontend with Host Header: test.localhost

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ curl -H "Host:test.localhost" http://127.0.0.1:80
Hostname: 88c29de3aeb0
IP: 127.0.0.1
IP: 10.0.0.6
IP: 10.0.0.7
IP: 172.18.0.3
GET / HTTP/1.1
Host: test.localhost
User-Agent: curl/7.47.0
Accept: */*
Accept-Encoding: gzip
X-Forwarded-For: 10.255.0.2
X-Forwarded-Host: test.localhost
X-Forwarded-Proto: http
X-Forwarded-Server: 2f827d04fbfb

Test Frontend with PathPrefix: /test

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ curl http://127.0.0.1:80/test
Hostname: 14bd4dc0ab00
IP: 127.0.0.1
IP: 10.0.0.12
IP: 10.0.0.13
IP: 172.18.0.4
GET /test HTTP/1.1
Host: 127.0.0.1
User-Agent: curl/7.47.0
Accept: */*
Accept-Encoding: gzip
X-Forwarded-For: 10.255.0.2
X-Forwarded-Host: 127.0.0.1
X-Forwarded-Proto: http
X-Forwarded-Server: 2f827d04fbfb

Test with failure expected:

1
2
3
4
5
$ curl http://127.0.0.1:80
404 page not found

$ curl -H "Host:foo.localhost" http://127.0.0.1:80
404 page not found

Change the frontend rule to foo.localhost and test again:

1
$ curl -XPUT http://127.0.0.1:8500/v1/kv/traefik/frontends/frontend1/routes/test_1/rule -d 'Host:foo.localhost'

Testing with foo.localhost :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ curl -H "Host:foo.localhost" http://127.0.0.1:80
Hostname: 88c29de3aeb0
IP: 127.0.0.1
IP: 10.0.0.6
IP: 10.0.0.7
IP: 172.18.0.3
GET / HTTP/1.1
Host: test.localhost
User-Agent: curl/7.47.0
Accept: */*
Accept-Encoding: gzip
X-Forwarded-For: 10.255.0.2
X-Forwarded-Host: foo.localhost
X-Forwarded-Proto: http
X-Forwarded-Server: 2f827d04fbfb

Test Flask Web Apps, with RoundRobin + Weight:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ curl -H "Host:flask.localhost" http://127.0.0.1:80
Container Hostname: c94e41420ec7 , UUID: 8d536277-1041-4140-b28a-91630d69ab15

$ curl -H "Host:flask.localhost" http://127.0.0.1:80
Container Hostname: 786a3b15a32e , UUID: 50bb435f-dac3-4cb0-8ecf-250f13a4d7a5

$ curl -H "Host:flask.localhost" http://127.0.0.1:80
Container Hostname: c94e41420ec7 , UUID: 680ca88c-8048-4044-a31b-1e922545dc8c

$ curl -H "Host:flask.localhost" http://127.0.0.1:80
Container Hostname: c94e41420ec7 , UUID: 5506792c-04e5-4517-b58e-fad57b5d1da5

$ curl -H "Host:flask.localhost" http://127.0.0.1:80
Container Hostname: 786a3b15a32e , UUID: 930056e5-4667-4c2c-976b-246cf891a351

$ curl -H "Host:flask.localhost" http://127.0.0.1:80
Container Hostname: c94e41420ec7 , UUID: 7c757b50-0e3a-47e2-8636-ae0583802afb

$ curl -H "Host:flask.localhost" http://127.0.0.1:80
Container Hostname: c94e41420ec7 , UUID: 918bf337-1476-480c-aacb-72fc89392c45

$ curl -H "Host:flask.localhost" http://127.0.0.1:80
Container Hostname: 786a3b15a32e , UUID: 5c2c6a0e-f2ea-4d5f-a6ce-a7df2ef4886b

Data Persistent Test:

List the Stacks:

1
2
3
4
5
$ docker stack ls
NAME                SERVICES
apps                6
kvstore             1
proxy               1

Kill the Consul Container to ensure data is persistent:

1
$ docker kill $(docker ps -f name=consul -q)

Verify that the replica has been fulfilled to its desired state:

1
2
3
4
5
$ docker stack ps kvstore
ID                  NAME                   IMAGE                    NODE                DESIRED STATE       CURRENT STATE                ERROR                         PORTS
j80kxxei6lyx        kvstore_consul.1       progrium/consul:latest   ip-10-1-4-51        Running             Running about a minute ago
lsvww0z8g24c         \_ kvstore_consul.1   progrium/consul:latest   ip-10-1-4-51        Shutdown            Failed about a minute ago    "task: non-zero exit (137)"
amkmsfodslwk         \_ kvstore_consul.1   progrium/consul:latest   ip-10-1-4-51        Shutdown            Shutdown 33 minutes ago

Read the Value of the Backend3 URL Key:

1
2
$ curl -XGET http://127.0.0.1:8500/v1/kv/traefik/backends/backend3/servers/server1/url?raw
http://whoami5:5000

`

Test The Service:

1
2
$ curl -H "Host:flask.localhost" http://127.0.0.1:80
Container Hostname: 786a3b15a32e , UUID: 4cf985e0-3f47-4320-ac38-42745b00ba1e

Inspect Services:

Consul:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
$ docker service inspect kvstore_consul
[
    {
        "ID": "ppe2c1ld5eyvby6x62fr649z8",
        "Version": {
            "Index": 2097
        },
        "CreatedAt": "2017-10-03T12:40:27.342669337Z",
        "UpdatedAt": "2017-10-03T12:55:22.93080059Z",
        "Spec": {
            "Name": "kvstore_consul",
            "Labels": {
                "com.docker.stack.image": "progrium/consul",
                "com.docker.stack.namespace": "kvstore"
            },
            "TaskTemplate": {
                "ContainerSpec": {
                    "Image": "progrium/consul:latest@sha256:8cc8023462905929df9a79ff67ee435a36848ce7a10f18d6d0faba9306b97274",
                    "Labels": {
                        "com.docker.stack.namespace": "kvstore"
                    },
                    "Args": [
                        "-server",
                        "-bootstrap",
                        "-log-level",
                        "debug",
                        "-ui-dir",
                        "/ui"
                    ],
                    "Privileges": {
                        "CredentialSpec": null,
                        "SELinuxContext": null
                    },
                    "Mounts": [
                        {
                            "Type": "bind",
                            "Source": "/mnt/consul",
                            "Target": "/data"
                        }
                    ],
                    "StopGracePeriod": 10000000000,
                    "DNSConfig": {}
                },
                "Resources": {},
                "RestartPolicy": {
                    "Condition": "any",
                    "Delay": 5000000000,
                    "MaxAttempts": 0
                },
                "Placement": {
                    "Constraints": [
                        "node.role == manager"
                    ],
                    "Platforms": [
                        {
                            "Architecture": "amd64",
                            "OS": "linux"
                        }
                    ]
                },
                "Networks": [
                    {
                        "Target": "5q3puzw9pfxa5gokx2mx1kn9j",
                        "Aliases": [
                            "consul"
                        ]
                    }
                ],
                "ForceUpdate": 0,
                "Runtime": "container"
            },
            "Mode": {
                "Replicated": {
                    "Replicas": 1
                }
            },
            "UpdateConfig": {
                "Parallelism": 1,
                "FailureAction": "pause",
                "Monitor": 5000000000,
                "MaxFailureRatio": 0,
                "Order": "stop-first"
            },
            "RollbackConfig": {
                "Parallelism": 1,
                "FailureAction": "pause",
                "Monitor": 5000000000,
                "MaxFailureRatio": 0,
                "Order": "stop-first"
            },
            "EndpointSpec": {
                "Mode": "vip",
                "Ports": [
                    {
                        "Protocol": "tcp",
                        "TargetPort": 8400,
                        "PublishedPort": 8400,
                        "PublishMode": "ingress"
                    },
                    {
                        "Protocol": "tcp",
                        "TargetPort": 8500,
                        "PublishedPort": 8500,
                        "PublishMode": "ingress"
                    },
                    {
                        "Protocol": "udp",
                        "TargetPort": 53,
                        "PublishedPort": 8600,
                        "PublishMode": "ingress"
                    }
                ]
            }
        },
        "PreviousSpec": {
            "Name": "kvstore_consul",
            "Labels": {
                "com.docker.stack.image": "progrium/consul",
                "com.docker.stack.namespace": "kvstore"
            },
            "TaskTemplate": {
                "ContainerSpec": {
                    "Image": "progrium/consul:latest@sha256:8cc8023462905929df9a79ff67ee435a36848ce7a10f18d6d0faba9306b97274",
                    "Labels": {
                        "com.docker.stack.namespace": "kvstore"
                    },
                    "Args": [
                        "-server",
                        "-bootstrap",
                        "-log-level",
                        "debug",
                        "-ui-dir",
                        "/ui"
                    ],
                    "Privileges": {
                        "CredentialSpec": null,
                        "SELinuxContext": null
                    },
                    "Mounts": [
                        {
                            "Type": "bind",
                            "Source": "/mnt/consul",
                            "Target": "/data"
                        }
                    ]
                },
                "Resources": {},
                "Placement": {
                    "Platforms": [
                        {
                            "Architecture": "amd64",
                            "OS": "linux"
                        }
                    ]
                },
                "Networks": [
                    {
                        "Target": "5q3puzw9pfxa5gokx2mx1kn9j",
                        "Aliases": [
                            "consul"
                        ]
                    }
                ],
                "ForceUpdate": 0,
                "Runtime": "container"
            },
            "Mode": {
                "Replicated": {
                    "Replicas": 1
                }
            },
            "EndpointSpec": {
                "Mode": "vip",
                "Ports": [
                    {
                        "Protocol": "tcp",
                        "TargetPort": 8400,
                        "PublishedPort": 8400,
                        "PublishMode": "ingress"
                    },
                    {
                        "Protocol": "tcp",
                        "TargetPort": 8500,
                        "PublishedPort": 8500,
                        "PublishMode": "ingress"
                    },
                    {
                        "Protocol": "udp",
                        "TargetPort": 53,
                        "PublishedPort": 8600,
                        "PublishMode": "ingress"
                    }
                ]
            }
        },
        "Endpoint": {
            "Spec": {
                "Mode": "vip",
                "Ports": [
                    {
                        "Protocol": "tcp",
                        "TargetPort": 8400,
                        "PublishedPort": 8400,
                        "PublishMode": "ingress"
                    },
                    {
                        "Protocol": "tcp",
                        "TargetPort": 8500,
                        "PublishedPort": 8500,
                        "PublishMode": "ingress"
                    },
                    {
                        "Protocol": "udp",
                        "TargetPort": 53,
                        "PublishedPort": 8600,
                        "PublishMode": "ingress"
                    }
                ]
            },
            "Ports": [
                {
                    "Protocol": "tcp",
                    "TargetPort": 8400,
                    "PublishedPort": 8400,
                    "PublishMode": "ingress"
                },
                {
                    "Protocol": "tcp",
                    "TargetPort": 8500,
                    "PublishedPort": 8500,
                    "PublishMode": "ingress"
                },
                {
                    "Protocol": "udp",
                    "TargetPort": 53,
                    "PublishedPort": 8600,
                    "PublishMode": "ingress"
                }
            ],
            "VirtualIPs": [
                {
                    "NetworkID": "zz458844j2msbqa6a1g2es8re",
                    "Addr": "10.255.0.5/16"
                },
                {
                    "NetworkID": "5q3puzw9pfxa5gokx2mx1kn9j",
                    "Addr": "10.0.0.2/24"
                }
            ]
        },
        "UpdateStatus": {
            "State": "completed",
            "StartedAt": "2017-10-03T12:55:09.724524449Z",
            "CompletedAt": "2017-10-03T12:55:22.930760766Z",
            "Message": "update completed"
        }
    }
]

Traefik:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
$ docker service inspect proxy_traefik
[
    {
        "ID": "oqb0lyiprpwby9xkb4n1mn5kl",
        "Version": {
            "Index": 2037
        },
        "CreatedAt": "2017-10-03T12:40:37.696749025Z",
        "UpdatedAt": "2017-10-03T12:40:37.698038856Z",
        "Spec": {
            "Name": "proxy_traefik",
            "Labels": {
                "com.docker.stack.image": "traefik",
                "com.docker.stack.namespace": "proxy"
            },
            "TaskTemplate": {
                "ContainerSpec": {
                    "Image": "traefik:latest@sha256:90697fb79a104520f350a3a1db6402584f473301ab6d1a71d264758b65fa232e",
                    "Labels": {
                        "com.docker.stack.namespace": "proxy"
                    },
                    "Args": [
                        "--consul",
                        "--consul.endpoint=consul:8500"
                    ],
                    "Privileges": {
                        "CredentialSpec": null,
                        "SELinuxContext": null
                    },
                    "StopGracePeriod": 10000000000,
                    "DNSConfig": {}
                },
                "Resources": {},
                "RestartPolicy": {
                    "Condition": "any",
                    "Delay": 5000000000,
                    "MaxAttempts": 0
                },
                "Placement": {
                    "Platforms": [
                        {
                            "Architecture": "amd64",
                            "OS": "linux"
                        },
                        {
                            "OS": "linux"
                        },
                        {
                            "Architecture": "arm64",
                            "OS": "linux"
                        }
                    ]
                },
                "Networks": [
                    {
                        "Target": "5q3puzw9pfxa5gokx2mx1kn9j",
                        "Aliases": [
                            "traefik"
                        ]
                    }
                ],
                "ForceUpdate": 0,
                "Runtime": "container"
            },
            "Mode": {
                "Replicated": {
                    "Replicas": 1
                }
            },
            "UpdateConfig": {
                "Parallelism": 1,
                "FailureAction": "pause",
                "Monitor": 5000000000,
                "MaxFailureRatio": 0,
                "Order": "stop-first"
            },
            "RollbackConfig": {
                "Parallelism": 1,
                "FailureAction": "pause",
                "Monitor": 5000000000,
                "MaxFailureRatio": 0,
                "Order": "stop-first"
            },
            "EndpointSpec": {
                "Mode": "vip",
                "Ports": [
                    {
                        "Protocol": "tcp",
                        "TargetPort": 80,
                        "PublishedPort": 80,
                        "PublishMode": "ingress"
                    },
                    {
                        "Protocol": "tcp",
                        "TargetPort": 8080,
                        "PublishedPort": 8080,
                        "PublishMode": "ingress"
                    }
                ]
            }
        },
        "Endpoint": {
            "Spec": {
                "Mode": "vip",
                "Ports": [
                    {
                        "Protocol": "tcp",
                        "TargetPort": 80,
                        "PublishedPort": 80,
                        "PublishMode": "ingress"
                    },
                    {
                        "Protocol": "tcp",
                        "TargetPort": 8080,
                        "PublishedPort": 8080,
                        "PublishMode": "ingress"
                    }
                ]
            },
            "Ports": [
                {
                    "Protocol": "tcp",
                    "TargetPort": 80,
                    "PublishedPort": 80,
                    "PublishMode": "ingress"
                },
                {
                    "Protocol": "tcp",
                    "TargetPort": 8080,
                    "PublishedPort": 8080,
                    "PublishMode": "ingress"
                }
            ],
            "VirtualIPs": [
                {
                    "NetworkID": "zz458844j2msbqa6a1g2es8re",
                    "Addr": "10.255.0.7/16"
                },
                {
                    "NetworkID": "5q3puzw9pfxa5gokx2mx1kn9j",
                    "Addr": "10.0.0.4/24"
                }
            ]
        }
    }
]

Flask App:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
$ docker service inspect apps_whoami5
[
    {
        "ID": "hy0443u03j8pygaegdorsrjot",
        "Version": {
            "Index": 2052
        },
        "CreatedAt": "2017-10-03T12:41:06.582341297Z",
        "UpdatedAt": "2017-10-03T12:41:06.583398389Z",
        "Spec": {
            "Name": "apps_whoami5",
            "Labels": {
                "com.docker.stack.image": "rbekker87/flask-containername",
                "com.docker.stack.namespace": "apps"
            },
            "TaskTemplate": {
                "ContainerSpec": {
                    "Image": "rbekker87/flask-containername:latest@sha256:fa4dc5905a10130d4309ffbc877155b9f61956980dc51ee2eaa16ac4255bcc2b",
                    "Labels": {
                        "com.docker.stack.namespace": "apps"
                    },
                    "Privileges": {
                        "CredentialSpec": null,
                        "SELinuxContext": null
                    },
                    "StopGracePeriod": 10000000000,
                    "DNSConfig": {}
                },
                "Resources": {},
                "RestartPolicy": {
                    "Condition": "any",
                    "Delay": 5000000000,
                    "MaxAttempts": 0
                },
                "Placement": {
                    "Platforms": [
                        {
                            "Architecture": "amd64",
                            "OS": "linux"
                        }
                    ]
                },
                "Networks": [
                    {
                        "Target": "5q3puzw9pfxa5gokx2mx1kn9j",
                        "Aliases": [
                            "whoami5"
                        ]
                    }
                ],
                "ForceUpdate": 0,
                "Runtime": "container"
            },
            "Mode": {
                "Replicated": {
                    "Replicas": 1
                }
            },
            "UpdateConfig": {
                "Parallelism": 1,
                "FailureAction": "pause",
                "Monitor": 5000000000,
                "MaxFailureRatio": 0,
                "Order": "stop-first"
            },
            "RollbackConfig": {
                "Parallelism": 1,
                "FailureAction": "pause",
                "Monitor": 5000000000,
                "MaxFailureRatio": 0,
                "Order": "stop-first"
            },
            "EndpointSpec": {
                "Mode": "vip"
            }
        },
        "Endpoint": {
            "Spec": {
                "Mode": "vip"
            },
            "VirtualIPs": [
                {
                    "NetworkID": "5q3puzw9pfxa5gokx2mx1kn9j",
                    "Addr": "10.0.0.8/24"
                }
            ]
        }
    }
}

Basic Example With Python to Create a Module That Consists of Classes and Functions

Just a very basic example how to create a Python Module that consists of a Single Class and 2 basic functions.

Our main app will can our module to print out a word, that we pass to our first function.

The Directory Setup:

Below is a tree view of my current working directory:

1
2
3
4
5
6
7
$ tree
.
├── providers
│   ├── __init__.py
│   ├── test.py
├── README.md
└── main.py

In order to make a python file a module, we need to have a blank __init__.py file in our directory. So any files under our providers directory will be seen as modules from our main.py file.

Our Test Module:

in our providers/test.py file:

1
2
3
4
5
6
7
8
class TestClass:

    def word_to_return(self, word_value):
        return word_value

    def simple_test(self):
        data = self.word_to_return('its me!')
        return data

Then our providers/test.py file will be blank.

Our main.py, we will import our test module, instantiate our class, and call our function within the class that we instantiated:

1
2
3
4
5
6
from providers import test

test_instance = test.TestClass()
response = test_instance.simple_test()

print(response)

instead of response = test_instance.simple_test(), you could also do print(test_instance.simple_test()

Testing it out:

1
2
$ python main.py
its me!

It’s very basic but will post some more topics around this in the future.

Also note, this blog is for quick posts that I come accross during my daily doings, for more details tutorials have a look at my main blog: sysadmins.co.za

Python Script to Decrypt Encrypted Data With AWS KMS

Quick script to decrypt data that was encrypted with your KMS key:

The Script:

The script requires the encrypted scring as an argument:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/env python

import boto3
import sys
from base64 import b64decode

try:
    encrypted_value = sys.argv[1]
except IndexError:
    print("Usage: {} {}".format(sys.argv[0], 'the-encrypted-string'))
    exit(1)

session = boto3.Session(
        region_name='eu-west-1',
        profile_name='default'
    )

kms = session.client('kms')

response = kms.decrypt(CiphertextBlob=b64decode(encrypted_value))['Plaintext']
print("Decrypted Value: {}".format(response))

Change the permissions so that the file is executable:

1
$ chmod +x decrypt.py

Usage:

1
2
$ ./decrypt.py asdlaskjdasidausd09q3uoijad09ujd38u309
Decrypted Value: thisIsMyDecryptedValue