Deployment Automation using Docker Compose

Goal - we will explore a use case where we have a two tier application - web tier and db tier. We will use docker to build deploy both together.



Directory tree

$ tree ~/docker-app/

/home/training/docker-app/

├── cassandra

│ ├── Dockerfile

│ └── init.cql

├── docker-compose.yml

└── flask-app

├── app.py

├── Dockerfile

└── requirements.txt



Create docker-compose file. It defines two services - web-service and db-service. DB-service depends on web-service. DB_service hosts a cassandra database. Data of cassandra is stored on /var/lib/cassandra/data. This data directory is mapped to a volume on the docker host machine - with a purpose that if the container is recreated, we want to persist the data outside the container environment.

$ cat ~/docker-app/docker-compose.yml

version: "3"

services:

web-service:

build: ./web-service

ports:

- "5000:5000"

volumes:

- "./web-service:/app"

depends_on:

- db-service


db-service:

build: ./db-service

volumes:

- "/data:/var/lib/cassandra/data"


Define a dockerfile for DB-service.

$ cat ~/docker-app/db-service/Dockerfile

FROM cassandra:3.11.3

VOLUME /var/lib/cassandra/data

USER cassandra

RUN cassandra


Define a docker file web service. We are using a customized ubuntu instance that consists of python and python libraries - flask web framework and cassandra-driver - as the name indicates it is a python driver for Cassandra database.

$ cat ~/docker-app/web-service/Dockerfile

FROM abasar/web_tier:1.0

COPY . /app

WORKDIR /app

CMD ["python", "app.py"]


Below is code of a web application - it exposes three path

  • / - show hello world

  • / init - initializes the database with requires schema

  • /counter - increment view count and show the current count

$ cat ~/docker-app/web-service/app.py

from cassandra.cluster import Cluster, NoHostAvailable

import socket


from flask import Flask

app = Flask(__name__)


session_ = None

db_host_ = "db-service"



def get_session():

global session_

if session_:

return session_

try:

socket.gethostbyname(db_host_)

cluster = Cluster([db_host_])

session_ = cluster.connect()

except socket.error:

print("DB host is not found")

except NoHostAvailable:

print("DB host is up but the DB service is not available")

return session_

@app.route("/", methods = ["get", "post"])

def hello():

return "Hello world"


@app.route("/init")

def init():

session = get_session()

if session:

session.execute("CREATE KEYSPACE IF NOT EXISTS demo WITH replication = {'class': 'SimpleStrategy','replication_factor': 1}")

session.execute("CREATE TABLE IF NOT EXISTS demo.counters(name text primary key, count counter)")

return "DB has been initialized"

return "DB Session is not active"


@app.route("/counter")

def counter():

session = get_session()

session.execute("update demo.counters set count += 1 where name = 'home'")

count = session.execute("select count from demo.counters where name = 'home'").one()[0]

return "Page view count: " + str(count)


if __name__ == "__main__":

app.run(debug = True, host = "0.0.0.0", port = 5000)


Validate the docker-compose.yml file

$ sudo docker-compose config

services:

db-service:

build:

context: /home/training/docker-app/db-service

volumes:

- /data:/var/lib/cassandra/data:rw

web-service:

build:

context: /home/training/docker-app/web-service

depends_on:

- db-service

ports:

- 5000:5000/tcp

volumes:

- /home/training/docker-app/web-service:/app:rw

version: '3.0'



$ sudo docker-compose build

Building db-service

Step 1/4 : FROM cassandra:3.11.3

---> d05bb5bbfe7d

Step 2/4 : VOLUME /var/lib/cassandra/data

---> Using cache

---> c80e5ea58ce9

Step 3/4 : USER cassandra

---> Using cache

---> 29e1c9c5b13b

Step 4/4 : CMD ["cassandra"]

---> Using cache

---> de307c093a33

Successfully built de307c093a33

Successfully tagged docker-app_db-service:latest

Building web-service

Step 1/4 : FROM abasar/web_tier:1.0

---> d6a39aa114c8

Step 2/4 : COPY . /app

---> Using cache

---> f70fccac3702

Step 3/4 : WORKDIR /app

---> Using cache

---> 117171fb6177

Step 4/4 : CMD ["python", "app.py"]

---> Using cache

---> 1ea7ab33e4bf

Successfully built 1ea7ab33e4bf

Successfully tagged docker-app_web-service:latest


You can find the files in the github link below.

https://github.com/abulbasar/docker-examples/tree/master/docker-app


Since we are using volume mapping from the DB service to /data, create the directory and give necessary permission - in this case we are giving full permission.

$ sudo mkdir /data

$ sudo chmod 777 /data


Start the services

$ sudo docker-compose up

Starting docker-app_db-service_1 ... done

docker-app_web-service_1 is up-to-date

Attaching to docker-app_db-service_1, docker-app_web-service_1

web-service_1 | * Serving Flask app "app" (lazy loading)

web-service_1 | * Environment: production

web-service_1 | WARNING: Do not use the development server in a production environment.

web-service_1 | Use a production WSGI server instead.

web-service_1 | * Debug mode: on

web-service_1 | * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)

web-service_1 | * Restarting with stat

web-service_1 | * Debugger is active!

...


Initialize the data base

$ curl localhost:5000/init

DB has been initialized

$ curl localhost:5000/counter

Page view count: 1


Simple stress testing

On host machine, install apache-utils

$ sudo apt install apache-utils

Run the stress test

$ ab -n 1000 -c 10 -k http://localhost:5000/counter

This is ApacheBench, Version 2.3 <$Revision: 1706008 $>

Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/

Licensed to The Apache Software Foundation, http://www.apache.org/


Benchmarking localhost (be patient)

Completed 100 requests

Completed 200 requests

Completed 300 requests

Completed 400 requests

Completed 500 requests

Completed 600 requests

Completed 700 requests

Completed 800 requests

Completed 900 requests

Completed 1000 requests

Finished 1000 requests



Server Software: Werkzeug/0.14.1

Server Hostname: localhost

Server Port: 5000


Document Path: /counter

Document Length: 19 bytes


Concurrency Level: 10

Time taken for tests: 8.359 seconds

Complete requests: 1000

Failed requests: 984

(Connect: 0, Receive: 0, Length: 984, Exceptions: 0)

Keep-Alive requests: 0

Total transferred: 178067 bytes

HTML transferred: 20067 bytes

Requests per second: 119.64 [#/sec] (mean)

Time per request: 83.586 [ms] (mean)

Time per request: 8.359 [ms] (mean, across all concurrent requests)

Transfer rate: 20.80 [Kbytes/sec] received


Connection Times (ms)

min mean[+/-sd] median max

Connect: 0 0 0.4 0 6

Processing: 35 83 100.4 69 1174

Waiting: 35 82 100.5 68 1173

Total: 35 83 100.9 69 1179


Percentage of the requests served within a certain time (ms)

50% 69

66% 75

75% 81

80% 85

90% 101

95% 115

98% 151

99% 861

100% 1179 (longest request)