Skip to content

3. Docker Compose 서비스 튜토리얼


1. Mission

001


  • Client에서 POST 방식으로 JSON 형식의 데이터를 전송한다.
  • 이때 JSON의 내용은 {"a": 1, "b": 2, "operator": "+"}와 같이 사칙연산에 필요한 데이터이며, 사칙연산 후 결괏값을 반환받아야 한다.
  • Docker Compose로 Proxy Server(Nginx), Calculate Server(Gunicorn 및 Flask), Cache Server(Redis), DB Server(MongoDB)를 구성한다.
  • Nginx는 로드 밸런싱 등의 기능을 사용하기 위함이다.
  • Gunicorn은 WSGI 미들웨어로 멀티 프로세싱이 가능하도록 하며, 이때 Gunicorn과 연결되는 웹 애플리케이션은 동기 방식의 Flask로 구성한다.
  • Redis는 요청된 데이터를 미리 확인하여 만약 한 번이라도 연산되었던 피연산자와 연산자에 대한 데이터인 경우 연산 과정 없이 결괏값을 바로 반환하기 위함이다.
  • MongoDB는 모든 피연산자와 연산자, 그리고 결괏값에 대한 데이터를 저장하기 위함이다.


2. Docker Compose 설치 및 확인

1) docker compose 사용 가능 여부 확인

  • 최신 버전의 Docker에서는 sudo docker compose를 사용할 수 있는데, 먼저 확인해 보자.


sudo docker compose
docker: 'compose' is not a docker command.
See 'docker --help'
sudo docker --version
Docker version 19.03.4


  • 위와 같이 확인되는 것은 예전 버전이다.
  • 이런 경우 sudo docker-compose를 사용할 수 있도록 별도로 설치해야 한다.


2) docker-compose 설치

  • Docker 공식 웹 사이트에서 설치 방법을 확인할 수 있다.



  • 리눅스 환경에서 설치하는 방법은 먼저 현재 안정적인 릴리스를 다운로드한다.


sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose


  • 그리고 바이너리에 실행 권한을 적용한다.


sudo chmod +x /usr/local/bin/docker-compose


  • 설치 후 만약 docker-compose 명령이 실패하면 경로를 확인한다.
  • /usr/bin 또는 다른 디렉토리에 대한 심볼릭 링크를 생성할 수도 있다.


sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose


  • 설치가 완료되었는지 확인한다.


$ sudo docker-compose --version
docker-compose version 1.29.2


  • 만약 설치된 docker-compose를 삭제하려면 다음과 같이 실행한다.


sudo rm /usr/local/bin/docker-compose


  • 참고로 docker-compose의 명령어 목록은 다음과 같다.


Commands:
  build              Build or rebuild services
  config             Validate and view the Compose file
  create             Create services
  down               Stop and remove resources
  events             Receive real time events from containers
  exec               Execute a command in a running container
  help               Get help on a command
  images             List images
  kill               Kill containers
  logs               View output from containers
  pause              Pause services
  port               Print the public port for a port binding
  ps                 List containers
  pull               Pull service images
  push               Push service images
  restart            Restart services
  rm                 Remove stopped containers
  run                Run a one-off command
  scale              Set number of containers for a service
  start              Start services
  stop               Stop services
  top                Display the running processes
  unpause            Unpause services
  up                 Create and start containers
  version            Show version information and quit


3. Docker Compose 구성하기

1) 이미지 가져오기

  • Docker Compose를 구성하기 위한 이미지를 가져온다.


sudo docker pull python && \
  sudo docker pull nginx && \
  sudo docker pull redis && \
  sudo docker pull mongo


2) Docker Compose 구성 파일 목록

  • 먼저 docker-compose라는 디렉터리를 생성하는데, 이때 디렉터리의 위치는 중요하지 않다.


mkdir docker-compose


  • 준비해야 할 Docker Compose 구성 파일 목록은 다음과 같다.


.
├── app.py
├── docker-compose.yml
├── Dockerfile
├── gunicorn.conf.py
└── nginx.conf


3) app 서비스

  • 먼저 app 서비스부터 준비할 것인데 Python 스크립트는 다른 서비스가 준비된 후에 작성하는 것이 좋다.


from flask import Flask, request
from pymongo import MongoClient
from bson.objectid import ObjectId
import redis
import hashlib
import requests
import json
import os

app = Flask(__name__)
redis_client = redis.StrictRedis(host="cache", port=6379, db=0)
mongo_client = MongoClient(host="db", port=27017)
mongo_db = mongo_client["test_db"]
mongo_collection = mongo_db["test_collection"]

@app.route("/", methods=["POST"])
def main():
    body = request.get_json(force=True)
    str_a = body["a"]
    str_b = body["b"]
    operator = body["operator"]

    int_a = int(str_a)
    int_b = int(str_b)

    if operator == "+":
        result = int_a + int_b
    elif operator == "-":
        result = int_a - int_b
    elif operator == "*":
        result = int_a * int_b
    elif operator == "/":
        result = int_a / int_b

    result = str(result)

    h = hashlib.new("sha256")
    h.update(str_a.encode("utf-8"))
    h.update(str_b.encode("utf-8"))
    h.update(operator.encode("utf-8"))
    h.update(result.encode("utf-8"))
    hashed_key = h.hexdigest()

    data_dict = {"a": str_a,
                 "b": str_b,
                 "operator": operator,
                 "result": result}
    dict_value = json.dumps(data_dict, ensure_ascii=False).encode("utf-8")

    redis_client.set(hashed_key, dict_value)
    mongo_collection.insert_one(data_dict)

    print(f"{str_a} {operator} {str_b} = {result}")

    return result

if __name__ == "__main__":
    app.run(host="0.0.0.0", port="80", debug=True)


  • Docker의 python 이미지를 빌드하여 사용할 것이기 때문에 Dockerfile을 작성한다.


FROM python:latest
LABEL APP Server
MAINTAINER VoicePrint

RUN pip install redis pymongo
RUN pip install requests gunicorn flask gevent


  • WSGI로는 Gunicorn을 사용할 것이기 때문에 gunicorn.conf.py 파일을 작성한다.


gunicorn.conf.py
import multiprocessing

# Python Module:Variable
wsgi_app = "app:app"

# Host:Port
bind = "0.0.0.0:9004"

# Worker Class
worker_class = "gevent"

# Num of Workers
# workers = int(multiprocessing.cpu_count() * 0.1)
workers = 1

# Num of Worker Connections
worker_connections = 1024

# Timeout == 0(Deactivate)
timeout = 0


4) proxy 서비스

  • Nginx 이미지를 사용하여 proxy 서비스를 구성할 것이다.
  • 이때 필요한 설정에 맞게 nginx.conf 파일을 작성한다.


nginx.conf
user  nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log notice;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
    use epoll;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    upstream application {
        server app:9004;
    }

    server {
        listen 9003;
        server_name localhost;

        location / {
            proxy_pass http://application;
        }

        error_page 500 502 503 504 /50x.html;
        location = /50x.html {
            root /usr/share/nginx/html;
        }
    }

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    #gzip  on;

    include /etc/nginx/conf.d/*.conf;
}


5) docker-compose.yml 작성

  • 이제 docker-compose.yml을 작성할 것인데, 이때 networks를 추가로 생성하여 정적 IP를 유지하도록 할 것이다.
  • cachedb에 대한 설정 파일은 작성하지 않지만, 필요한 경우 작성해서 volumes로 연결하면 된다.


docker-compose.yml
version: "3.8"

services:
  app:
    container_name: "app"
    build:
      context: .
      dockerfile: ./Dockerfile
    restart: always
    ports:
      - "9004:9004"
    depends_on:
      - cache
      - db
    working_dir: /Project
    entrypoint: ["gunicorn"]
    volumes:
      - ./gunicorn.conf.py:/Project/gunicorn.conf.py
      - ./app.py:/Project/app.py
    networks:
      app_net:
        ipv4_address: 172.16.238.3

  proxy:
    container_name: "proxy"
    image: nginx:latest
    restart: always
    ports:
      - "9003:9003"
    depends_on:
      - app
    working_dir: /
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
    networks:
      app_net:
        ipv4_address: 172.16.238.2

  cache:
    container_name: "cache"
    image: redis:latest
    restart: always
    ports:
      - "6379:6379"
    working_dir: /
    entrypoint: ["redis-server"]
    volumes:
      - /data:/db
    networks:
      app_net:
        ipv4_address: 172.16.238.4

  db:
    container_name: "db"
    image: mongo:latest
    restart: always
    ports:
      - "27017:27017"
    working_dir: /
    volumes:
      - /data:/data/db
    networks:
      app_net:
        ipv4_address: 172.16.238.5

networks:
  app_net:
    ipam:
      driver: default
      config:
        - subnet: "172.16.238.0/24"


4. Docker Compose 실행하기

  • 이제 docker-compose 명령어를 사용하여 위에서 작성한 docker-compose.yml을 실행시킨다.


sudo docker-compose -p test up -d


  • 실행 순서는 cachedbappproxy 순이다.


002


5. Docker Compose 서비스 테스트

  • 별도의 터미널을 띄워 다음과 같이 테스트를 해본다.


json='{"a": "1", "b": "1", "operator": "+"}' && \
curl -X POST \
     http://14.138.218.153:9003 \
    -H 'Content-type: application/json' \
    -d "$json"


  • 결과는 다음과 같다.


2%


  • 실제로 cachedb에 데이터가 저장되었는지 확인한다.


sudo docker exec -it app /bin/bash


  • test.py라는 이름의 스크립트를 작성할 것인데, 그전에 에디터를 설치해야 한다.


apt-get update && apt-get install vim -y


  • test.py 스크립트를 작성한다.


test.py
from pymongo import MongoClient
from bson.objectid import ObjectId
import redis

redis_client = redis.StrictRedis(host="cache", port=6379, db=0)
mongo_client = MongoClient(host="db", port=27017)
mongo_db = mongo_client["test_db"]
mongo_collection = mongo_db["test_collection"]

keys = redis_client.keys()
keys = [key.decode("utf-8") for key in keys]
vals = redis_client.mget(keys)
vals = [val.decode("utf-8") for val in vals]
kv = dict(zip(keys, vals))
print(kv)

for document in mongo_collection.find():
    print(document)


  • test.py 스크립트를 실행해 보면 cachedb에 잘 저장되어 있는 것을 확인할 수 있다.


python test.py
{'3ad3a37b3c9a4d6e65b58c4a937e65277eba441a807a1f3df49e20011a753539': '{"a": "1", "b": "1", "operator": "+", "result": "2"}'}
{'_id': ObjectId('618393880dfb22aee9700a1e'), 'a': '1', 'b': '1', 'operator': '+', 'result': '2'}