FIT5225 Assignment 1 - Docker & Kubernetes

This report is related to FIT5225 2021S1 Assignment 1. In this assignment, we created a web service by using Docker & Kubernetes.


Assignment Requirements

You can find it in: https://github.com/RdWeirdo981/Master-of-Cybersecurity/blob/main/2021S1%20FIT5225%20Cloud%20Computing%20and%20Security/A1/FIT5225_Assignment1__2021%20(1).pdf

Assignment Steps

Create instance

  1. Key: xx-A1
  2. Security group: add IPv4 TCP 8080 0.0.0.0/0
  3. Upload: files

Install package

  1. install pip3
1
2
3
$ sudo apt update
$ sudo apt install python3-pip
$ pip3 --version
  1. install numpy
1
$ pip3 install numpy
  1. install opencv
1
2
3
4
$ pip3 install --upgrade pip setuptools wheel
$ pip3 install opencv-python
$ sudo apt update
$ sudo apt install libgl1-mesa-glx
  1. install flask
1
$ pip3 install Flask

The web service

  1. Run server.py
  2. Send requests by client.py
  3. Success!

Docker file

  1. Give the user the permission
1
$ sudo usermod -aG docker ${USER}

and re-connect.

  1. Go to folder containing server.py, dockerfile, requirements.txt, and yolo_tiny_configs folder.
  2. Encapsulate
1
$ docker build -t server_app .
  1. Run the docker image in a container
1
$ docker run -i -t -p 5000:5000 --name my_server server_app
  1. Test by run the client.py
  2. Success!

publish the docker image

  1. Create an account & login
  2. Create a repo
  3. Login on the VM:
1
$ docker login --username=[your un] --password=[your pw]
  1. tag the image:
1
2
$ docker images
$ docker tag fdfd0ebb119f [username]/app:v1
  1. push image:
1
$ docker push [username]/app

Kubernetes Cluster

install kubectl

cmd:

1
2
3
$ curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
$ sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
$ kubectl version --client

install Kind

cmd:

1
2
3
4
$ curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.10.0/kind-linux-amd64
$ chmod +x ./kind
$ sudo mv ./kind /usr/local/bin/kind
$ kind

create a Kuber cluster

1 Kuber cluster --> 1 controller + 1 worker

  1. create a cluster: 1 controller + 1 worker

go to the folder.

1
2
3
$ kind create cluster --config kind_setup_config.yml
$ kubectl cluster-info --context kind-kind
$ kubectl get nodes

*Note:

--name: can't be capital letter.

container port.

info:

1
2
3
4
Kubernetes control plane is running at https://127.0.0.1:36549
KubeDNS is running at https://127.0.0.1:36549/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
  1. delete a cluster

you have to stop and delete all the containers

1
2
$ kind delete cluster (--name )
$ sudo swapon --show

Kubernetes Service

deployment

  1. write deployment.yml, transfer it to the local path
  2. launch deployment
1
2
3
$ kubectl apply -f deployment.yml
$ kubectl describe pod # see what's wrong
$ kubectl scale deployment iweb-deployment --replicas=4 // change # of pods
  1. check the current Kuber:
1
$ kubectl get nodes,deployments,pods

*Note:

notice the docker hub path.

change replicas to change # of pods

service

  1. write service.yml
  2. launch service
1
$ kubectl apply -f service.yml
  1. get info of service
1
$ kubectl get svc -o wide
  1. test
1
$ curl http://hostip:80

Experiement

Note:

swapfile is full:

1
2
3
$ df -Th
$ df -i
$ sudo swapon --show

Related Code

iWeb App

This is the front-end code for client. Notice that I wrote it as an automated process so that we don't have to run it much time.

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
# Generate the parallel requests based on the ThreadPool Executor
from concurrent.futures import ThreadPoolExecutor as PoolExecutor
import sys
import time
import glob
import requests
import threading
import uuid
import base64
import json
import os

#send http request
def call_object_detection_service(image):
try:

url = "http://118.138.245.115:80/server"
data = {}
#generate uuid for image
id = uuid.uuid5(uuid.NAMESPACE_OID, image)
# Encode image into base64 string
with open (image, 'rb') as image_file:
data['image'] = base64.b64encode(image_file.read()).decode('utf-8')

data ['id'] = str(id)

headers = {'Content-Type': 'application/json'}

response = requests.post(url, json = json.dumps(data), headers = headers)

if response.ok:
output = "Thread : {}, input image: {}, output:{}".format(threading.current_thread().getName(),
image, response.text)
print(output)
else:
print ("Error, response status:{}".format(response))

except Exception as e:
print("Exception in webservice call: {}".format(e))

# gets list of all images path from the input folder
def get_images_to_be_processed(input_folder):
images = []
for image_file in glob.iglob(input_folder + "*.jpg"):
images.append(image_file)
return images

def main():
## provide argumetns-> input folder, url, number of wrokers

path = "inputfolder/"
images = get_images_to_be_processed(path)
num_images = images.__len__()

thread_list = [1,6,11,16,21,26,31]
response_time = []
for thread in thread_list:
for _ in range(3):
start_time = time.time()
#craete a worker thread to invoke the requests in parallel
with PoolExecutor(max_workers=thread) as executor:
for _ in executor.map(call_object_detection_service, images):
pass
elapsed_time = time.time() - start_time
response_time.append("Thread {}: Total time spent: {}, average response time: {} ".format(thread,elapsed_time, elapsed_time/num_images))
for item in response_time:
print(item)

if __name__ == "__main__":
main()

This is the back-end code for server:

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
# import the necessary packages
import numpy as np
import time
import cv2
import os
import json
from flask import Flask, request, jsonify
import base64

# construct the argument parse and parse the arguments
confthres = 0.3
nmsthres = 0.1


def get_labels(labels_path):
# load the COCO class labels our YOLO model was trained on
lpath = os.path.sep.join([yolo_path, labels_path])

print(yolo_path)
LABELS = open(lpath).read().strip().split("\n")
return LABELS


def get_weights(weights_path):
# derive the paths to the YOLO weights and model configuration
weightsPath = os.path.sep.join([yolo_path, weights_path])
return weightsPath


def get_config(config_path):
configPath = os.path.sep.join([yolo_path, config_path])
return configPath


def load_model(configpath, weightspath):
# load our YOLO object detector trained on COCO dataset (80 classes)
print("[INFO] loading YOLO from disk...")
net = cv2.dnn.readNetFromDarknet(configpath, weightspath)
return net


def do_prediction(image, net, LABELS):
(H, W) = image.shape[:2]
# determine only the *output* layer names that we need from YOLO
ln = net.getLayerNames()
ln = [ln[i[0] - 1] for i in net.getUnconnectedOutLayers()]

# construct a blob from the input image and then perform a forward
# pass of the YOLO object detector, giving us our bounding boxes and
# associated probabilities
blob = cv2.dnn.blobFromImage(image, 1 / 255.0, (416, 416),
swapRB=True, crop=False)
net.setInput(blob)
start = time.time()
layerOutputs = net.forward(ln)
# print(layerOutputs)
end = time.time()

# show timing information on YOLO
print("[INFO] YOLO took {:.6f} seconds".format(end - start))

# initialize our lists of detected bounding boxes, confidences, and
# class IDs, respectively
boxes = []
confidences = []
classIDs = []

# loop over each of the layer outputs
for output in layerOutputs:
# loop over each of the detections
for detection in output:
# extract the class ID and confidence (i.e., probability) of
# the current object detection
scores = detection[5:]
# print(scores)
classID = np.argmax(scores)
# print(classID)
confidence = scores[classID]

# filter out weak predictions by ensuring the detected
# probability is greater than the minimum probability
if confidence > confthres:
# scale the bounding box coordinates back relative to the
# size of the image, keeping in mind that YOLO actually
# returns the center (x, y)-coordinates of the bounding
# box followed by the boxes' width and height
box = detection[0:4] * np.array([W, H, W, H])
(centerX, centerY, width, height) = box.astype("int")

# use the center (x, y)-coordinates to derive the top and
# and left corner of the bounding box
x = int(centerX - (width / 2))
y = int(centerY - (height / 2))

# update our list of bounding box coordinates, confidences,
# and class IDs
boxes.append([x, y, int(width), int(height)])

confidences.append(float(confidence))
classIDs.append(classID)

# apply non-maxima suppression to suppress weak, overlapping bounding boxes
idxs = cv2.dnn.NMSBoxes(boxes, confidences, confthres,
nmsthres)

# TODO Prepare the output as required to the assignment specification
# ensure at least one detection exists

response_dict = {} # empty dict for "objects": [{item1}, {item2}, ...]
response_list = []

if len(idxs) > 0:
# loop over the indexes we are keeping
for i in idxs.flatten():
item_dict = {} # empty dict for label, accuracy, rectangle
rectangle_dict = {}

item_dict["label"] = LABELS[classIDs[i]]
item_dict["accuracy"] = confidences[i]
rectangle_dict["height"] = boxes[i][3]
rectangle_dict["left"] = boxes[i][0]
rectangle_dict["top"] = boxes[i][1]
rectangle_dict["width"] = boxes[i][2]
item_dict["rectangle"] = rectangle_dict

response_list.append(item_dict)

response_dict["objects"] = response_list
return response_dict # return a dict

## Yolov3-tiny versrion
yolo_path = "yolo_tiny_configs/"
labelsPath = "coco.names"
cfgpath = "yolov3-tiny.cfg"
wpath = "yolov3-tiny.weights"

Lables = get_labels(labelsPath)
CFG = get_config(cfgpath)
Weights = get_weights(wpath)


# TODO, you should make this console script into webservice using Flask

app = Flask(__name__)

@app.route('/')
def hello():
return ("hello!")

@app.route('/server', methods = ['POST'])
def main():
try:
imagefile = request.get_data().decode()
imagefile = json.loads(json.loads(imagefile)) # imagefile now is a dict: {'id':xxxx, 'image': yyyy}
return_dict = {} # this dict will be: {'id':xxxx, 'objects': [] }

## get reqest id
return_dict['id'] = imagefile['id']

## get objects

## Decode base64
decode_img = base64.b64decode(imagefile['image'])
nparr = np.fromstring(decode_img, np.uint8)
image = cv2.imdecode(nparr, cv2.COLOR_BGR2GRAY)

# load the neural net. Should be local to this method as its multi-threaded endpoint
nets = load_model(CFG, Weights)
object_dict = do_prediction(image, nets, Lables)
return_dict['objects'] = object_dict['objects']

## jsonfy return_dict
return jsonify(return_dict)

except Exception as e:
print("Exception {}".format(e))
return jsonify({'objects': 'invalid'})


if __name__ == '__main__':
app.run(debug=True, threaded = True, host = '0.0.0.0')

Dockerfile

1
2
3
4
5
6
7
8
9
FROM python:3.7
WORKDIR /server
ADD iWebLens_server.py /server
COPY requirements.txt requirements.txt
COPY yolo_tiny_configs yolo_tiny_configs
RUN apt-get update
RUN apt-get install ffmpeg libsm6 libxext6 -y
RUN pip3 install -r requirements.txt
CMD ["python3", "/server/iWebLens_server.py"]

Kubernetes yaml

Setup yaml

1
2
3
4
5
6
7
8
9
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
extraPortMappings:
- containerPort: 31414
hostPort: 80
protocol: tcp
- role: worker

Deployment yaml

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
apiVersion: apps/v1
kind: Deployment
metadata:
name: iweb-deployment
labels:
app: iweb
spec:
replicas: 1
selector:
matchLabels:
app: iweb
template:
metadata:
labels:
app: iweb
spec:
containers:
- name: iweb
image: dkjrvdfr/a1-server-app:v1
ports:
- containerPort: 5000
resources:
requests:
cpu: "0.5"
limits:
cpu: "0.5"

Service yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: v1
kind: Service
metadata:
name: iweb-service
spec:
type: NodePort
selector:
app: iweb
ports:
- port: 80
targetPort: 5000
nodePort: 31414
protocol: TCP