태그 보관물: docker

Multi-container app의 Bad Gateway(502) 문제 디버깅

Intel Training and Learning Suite(TLS)는 처음으로 경험해보는 multi-container application이다. 중간에 native개발을 위한 환경 설정을 끼워 넣어 보려고 이것 저것 시도하는 중에 web interface를 담당하는 tls_proxy라는 docker가 Bad Gateway를 띄우면서 문제가 생겼다. 디버깅 하는 과정에서 사용해 본 docker-compose명령어 들의 쓰임새가 유용할 것 같아서 기록으로 남겨 둔다.

Port number와 docker찾기: docker-compose ps

Web browser로 서비스에 접근하면(https://localhost) Bad Gateway(502) error가 발생하는데 처음으로 web-browser의 접속 요청을 받아서 처리하는 docker container는 docker-compose ps 명령어로 docker image별로 listening하는 port number들을 찾을 수 있다.

TLS의 경우는 tls_proxy가 http요청 처리를 위한 80번과 https를 위한 443번 port를 listening하고 있다.

$ docker-compose ps
    Name                  Command               State     Ports                     
------------------------------------------------------------------
tls_apiui      ./webservices/start.sh           Exit 0                                                                                                                                                     
tls_core       ./start.sh                       Up                                                                                                                                                         
tls_mongo      docker-entrypoint.sh --tls ...   Up       127.0.0.1:27017->27017/tcp                                                                                                                        
tls_openvino   /bin/bash                        Exit 0                                                                                                                                                     
tls_proxy      /docker-entrypoint.sh /bin ...   Up       0.0.0.0:443->443/tcp,:::443->443/tcp, 80/tcp                                                                                                      
tls_rabbitmq   docker-entrypoint.sh /init ...   Up       0.0.0.0:15672->15672/tcp,:::15672->15672/tcp, 15691/tcp, 15692/tcp, 0.0.0.0:1883->1883/tcp,:::1883->1883/tcp, 25672/tcp, 4369/tcp, 5671/tcp, 5672/tcp, 0.0.0.0:8883->8883/tcp,:::8883->8883/tcp
tls_redis      docker-entrypoint.sh sh st ...   Up       127.0.0.1:6379->6379/tcp

Docker의 log 보기: docker logs tls_proxy

tls_proxy가 요청을 처리하지 못한 이유는 docker logs <서버 이름>으로 확인할 수 있다.

$ docker logs tls_proxy
2023/05/31 01:31:06 [error] 12#12: *5 connect() failed (113: Host is unreachable) while connecting to upstream, client: 172.31.0.1, server: , request: "GET / HTTP/1.1", upstream: "http://172.31.0.8:3000/", host: "localhost"
...

사설 네트워크인 172.31.0.x로 docker service들을 구성했는데, 그중 인터페이스를 담당하는 172.31.0.1 docker가 172.31.0.8에 요청을 연결해 주어야 하는데 이 부분에서 오류가 나고 있는 모양이다.

Docker network 확인: docker network ls

현재 docker용으로 구성되어 있는 network을 확인해 보면 다음과 같다.

$ docker network ls
NETWORK ID     NAME                DRIVER    SCOPE
d15b28e7a1d5   bridge              bridge    local
acc99b5947d1   cvat_cvat           bridge    local
4485972f8274   host                host      local
6f21c18ba66f   none                null      local
20d132ccfc2a   tls_default         bridge    local

각 네트워크의 구성을 검사(inspect)해 볼 수 있는데 docker network inspect cvat_cvat명령어로 해당 네트워크를 검사해 보면 다음과 같이 위에서 오류가 발생했던 ip인 172.21.0.8번(tls_openvino)이 등록되지 않은 것을 볼 수 있다.

$ docker network inspect cvat_cvat|grep "Name\|IPv4Address"
        "Name": "cvat_cvat",
                "Name": "cvat_redis",
                "IPv4Address": "172.31.0.4/16",
                "Name": "cvat_opa",
                "IPv4Address": "172.31.0.3/16",
                "Name": "cvat_db",
                "IPv4Address": "172.31.0.5/16",
                "Name": "traefik",
                "IPv4Address": "172.31.0.2/16",
                "Name": "cvat",
                "IPv4Address": "172.31.0.6/16",
                "Name": "cvat_ui",
                "IPv4Address": "172.31.0.7/16",
                "Name": "tls_proxy",
                "IPv4Address": "172.31.0.9/16",

Docker의 log 보기: docker logs tls_openvino

문제가 되고 있는 docker container를 특정했으니 docker logs tls_openvino명령어로 다시 한번 해당 container의 로그를 확인해 본다.

error: A hook (`userconfig`) failed to load!
error: Failed to lift app: Error: Attempted to `require('/home/tls/webservices/apiserver/config/env/production.js')`, but an error occurred:
--
Error: ENOENT: no such file or directory, open '../../thirdparty/security/ca/ca_certificate.pem'
    at Object.openSync (fs.js:498:3)
    at Object.readFileSync (fs.js:394:35)
    at Object.<anonymous> (/home/tls/webservices/apiserver/config/env/production.js:70:15)

../../third party/security/ca/ca_certificate.pem파일을 읽지 못해서 오류가 발생했다고 나오는데, 실제로 container에서는 해당 파일의 위치를 상대 경로로 접근하지 않는다. Call stack에 나와 있는 production.js line 70근처의 내용을 보면 먼저 절대경로인 /run/secrets/*에 접근을 시도할 때 예외가 발생해서 상대 경로로 접근을 시도하고(개발용 코드로 추정) 이 마저도 실패한 것이 로그에 나온 것이다.

apiserver/config/env/production.js

try {
  tls_ca = fs.readFileSync("/run/secrets/ca_tls")
  tls_server_cert = fs.readFileSync("/run/secrets/tlsserver_cert")
  tls_server_key = fs.readFileSync("/run/secrets/tlsserver_key")
} catch (err) {
  tls_ca = fs.readFileSync("../../thirdparty/security/ca/ca_certificate.pem");
  tls_server_cert = fs.readFileSync("../../thirdparty/security/TLS_server_cert.crt");
  tls_server_key = fs.readFileSync("../../thirdparty/security/TLS_server_key.pem");
}

정리하자면, 문제는 server secret을 생성하는 과정에서 발생한 것이 docker image상의 key file path read에서 오류를 발생시키고 tls_openvino container가 제대로 뜨지 못한 문제이다.

해결(?)

문제의 원인은 생각보다 싱거웠는데, docker-compose 명령어를 실행시키는 native용 script에서 다음 줄을 연결하기 위한 \를 빼먹는 사소한(!) Bash 문법 오류가 발생 했었고, 이 오류가 무시된 채로 docker-compose build 명령어가 계속 수행되어 service가 up되는 상황까지 된 것이었다.

-       && pip install -U pip wheel setuptools
+       && pip install -U pip wheel setuptools \
        && pip install \

위의 수정과 함께 오류가 생기면 build를 멈추도록 set -e를 bash script에 추가 하는 것으로 이 문제는 일단락 되었다.

CARLA client를 build해야 하는데 clang-8이 없댄다.

2022년 9월 현재 CARLA simulator의 최신 버전인 0.9.13으로 PythonAPI를 빌드하려고 하면 clang-8 version을 요구하는데 Ubuntu 22.04에서는 clang-8의 repository를 추가하고 설치 시도하면 다음과 같이 의존성 오류가 생긴다.

The following packages have unmet dependencies:
 clang-8 : Depends: libllvm8 (>= 1:8~svn298832-1~) but it is not going to be installed
           Depends: libstdc++-5-dev but it is not installable
           Depends: libgcc-5-dev but it is not installable
           Depends: libobjc-5-dev but it is not installable
           Depends: libclang-common-8-dev (= 1:8.0.1+svn369350-1~exp1~20200112113617.82) but it is not going to be installed
           Depends: libclang1-8 (= 1:8.0.1+svn369350-1~exp1~20200112113617.82) but it is not going to be installed
           Recommends: llvm-8-dev but it is not going to be installed
           Recommends: libomp-8-dev but it is not going to be installed
 lld-8 : Depends: libllvm8 (= 1:8.0.1+svn369350-1~exp1~20200112113617.82) but it is not going to be installed
E: Unable to correct problems, you have held broken packages.

그리고 아쉽게도 하위 호환에 문제가 있는 것인지 설치 가능한 최하 버전인 clang-10으로는 CARLA client build가 되지 않는다. 빌드 스크립트에서 버전 점검하는 부분을 건너 뛰도록 수정하고 clang-10으로 강제 빌드를 시도해봤더니 역시나 빌드오류가 나면서 일이 커질것 같다는 느낌이 강하게 든다.

일일이 빌드오류 잡는 삽질을 하고 ‘CARLA client build 삽질기’를 포스팅 할 수도 있었겠지만 이번에는 문명의 이기인 Docker를 한번 누려 보기로했다.

먼저, Docker로 clang-8 설치가 가능한 Ubuntu 18.04에서 빌드를 수행한 다음(참고 CARLA – Linux Build) 빌드가 완료되면 Python client package를 host에 설치한다.

  • Docker로 Ubuntu18.04를 설치하고 CARLA의 소스 코드위치를 /carla로 마운트하여 실행한다.
$ docker pull ubuntu:18.04
$ docker run -ti -v <carla-root-path>:/carla ubuntu:18.04 /bin/bash
  • Clang-8의 repository를 추가하고 관련된 패키지들을 설치한다.
# Clang-8 repository 추가.
DOCKER# apt-get update &&
apt-get install wget software-properties-common &&
add-apt-repository ppa:ubuntu-toolchain-r/test &&
wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key|apt-key add - &&
apt-add-repository "deb http://apt.llvm.org/xenial/ llvm-toolchain-xenial-8 main" &&
apt-get update

# Install packages.
DOCKER# apt-get install -y build-essential clang-8 lld-8 g++-7 cmake ninja-build libvulkan1 python python-pip python-dev python3-dev python3-pip libpng-dev libtiff5-dev libjpeg-dev tzdata sed curl unzip autoconf libtool rsync libxml2-dev git
  • Clang-8을 default로 설정한다.
# Set clang-8 as a default clang.
DOCKER# update-alternatives --install /usr/bin/clang++ clang++ /usr/lib/llvm-8/bin/clang++ 180 && update-alternatives --install /usr/bin/clang clang /usr/lib/llvm-8/bin/clang 180
  • Host machine의 python과 동일한 버전(3.8)을 docker에도 설치하고 관련된 Python package들을 pip로 설치해준다. 특별히 setuptools package는 버전의 영향을 받으므로 확인된 버전(47.3.1)을 명시해 준다.
DOCKER# apt install -y python3.8 python3.8-dev

# PIP upgrade
DOCKER# pip3 install --upgrade pip && pip install --upgrade pip

# 중요. Python3의 setuptools version이 안맞으면 빌드에 실패할 수 있으니 버전명을 명시해 준다.
DOCKER# pip2 install setuptools &&
pip3 install -Iv setuptools==47.3.1 &&
pip2 install distro &&
pip3 install distro &&
pip2 install wheel &&
pip3 install wheel auditwheel
  • 이제 해당 버전으로 docker에서 build를 시도 한다.
DOCKER# cd /carla
DOCKER# make PythonAPI ARGS="--python-version=3.8"
  • /carla/PythonAPI/carla/dist 아래에 소스에 포함되어 있는 3.6용 package외에 새롭게 빌드한 3.8용 *.whl, *.egg file들이 생성된 것을 확인하고 host system에 설치해 준다.
DOCKER# ls /carla/PythonAPI/carla/dist/
carla-0.9.13-cp36-cp36m-linux_x86_64.whl  carla-0.9.13-py3.6-linux-x86_64.egg
carla-0.9.13-cp38-cp38m-linux_x86_64.whl  carla-0.9.13-py3.8-linux-x86_64.egg

# CARLA client 설치
$ sudo apt install -y <carla-root-path>/PythonAPI/carla/dist/carla-0.9.13-py3.8-linux-x86_64.egg

Docker로 OpenGrok 설치

잘 쓰고 있던 OpenGrok 서버가 갑자기 맛이 가는 바람에 부랴부랴 대안을 찾아야 했는데 마땅한 서버가 없어서 로컬 머신에 Docker로 설치하는 방법을 찾아 보았다. 여기 소개된 내용은 Docker Hub에서 자세한 설명을 찾을 수 있다.

Docker가 설치되어 있다면 command창에서 다음의 명령으로 OpenGrok docker를 pull한다.

docker pull opengrok/docker

Pulling이 끝나면 목적에 소스와 indexing결과가 저장될 공간을 만들어 준다. src에는 분석할 소스를 넣고 bin에는 편의를 위한 스크립트를 넣을 예정이다.

mkdir -p ~/opengrok/bin
mkdir -p ~/opengrok/src
mkdir -p ~/opengrok/etc
mkdir -p ~/opengrok/data

이제, 8080 port에 접속 설정을 하고 위에서 만든 volume들을 docker에 마운트 시켜준다. Git server에 접근하기 위해 키 관련 설정을 해주어야 하는데, 귀찮아서 그냥 .ssh 디렉토리를 마운트 시켜 주었다.

docker run -d \
    --name opengrok \
    -p 8080:8080/tcp \
    -v ~/opengrok/bin/:/opengrok/bin/ \
    -v ~/opengrok/src/:/opengrok/src/ \
    -v ~/opengrok/etc/:/opengrok/etc/ \
    -v ~/opengrok/data/:/opengrok/data/ \
    -v ~/.ssh:/root/.ssh \
    opengrok/docker:latest

이제 해당 서버의 콘솔을 열고 인덱싱 명령을 수행하면 된다. GUI가 없다면 다음의 명령으로 실행 중인 docker에 접속할 수 있다.

docker exec -it <docker_container_id> bash

서버에 접속한 후 인덱싱을 수행하는 명령어는 다음과 같다.

export OPENGROK_DIR=/opengrok
java \
    -Djava.util.logging.config.file=$OPENGROK_DIR/etc/logging.properties \
    -Xmx1024m \
    -jar $OPENGROK_DIR/lib/opengrok.jar \
    -c /usr/local/bin/ctags \
    -s $OPENGROK_DIR/src -d $OPENGROK_DIR/data -H -P -S -G \
    -W $OPENGROK_DIR/etc/configuration.xml -U http://localhost:8080/

인덱싱이 끝나면 웹브라우져에서 http://localhost:8080으로 접속하면 된다.

위의 인덱싱 명령어가 너무 길어서 입력하기 힘들기 때문에 source code를 업데이트하고 인덱싱 하는 과정을 묶어서 다음과 같이 스크립트로 만들고 ~/opengrok/bin 안에 넣어 두면 편리하게 사용할 수 있다.

[Tip] Mac version docker에서 띄운 (웹) 서버에 접속하기

Mac version Docker에 띄워둔 web server에 host에서 접속하려면 어떻게 해야 할까? 실행할 때 ‘–network=host’를 주면 된다는 얘기가 있어서 해봤는데, Mac에서는 통하지 않았다 이건 linux용이라고… Networking features in Docker Desktop for Mac에 따라 실행할 때 port를 매핑하는 것으로 이 문제를 해결할 수 있다.

Docker의 9090 port에 Mac의 9090 port로 접근하려면 다음과 같이 -p option으로 port를 매핑해서 docker를 띄운 후 서버를 실행한다.

# Port 매핑으로 docker를 실행
MAC$ docker run --rm -ti -p 9090:9090 DOCKER_IMAGE

# Docker에서 웹서버를 띄운다. Python http.server module을 사용.
DOCKER$ python -m http.server 9090
Serving HTTP on 0.0.0.0 port 9090 (http://0.0.0.0:9090/) ...

이제 Mac의 web browser로 해당 포트에 접근할 수 있다.

Travis CI 설정과 docker image 사용

GitHub project에 CI를 붙이고 싶은데 Jenkins server가 회사 firewall 안에 들어 있어서 GitHub에서 직접 webhook을 붙일 수 없는 문제가 있다. Jenkins의 GitHub plugin으로 tunneling을 설정하는 방법 등 있기는 하지만 다른 CI 옵션들을 살펴 보던중 Open source project에 대해서는 무료라는 Travis CI가 있다는 것을 알게 되었다. Travis CI는 기본으로 Ubuntu를 지원하고 그 외의 경우는 docker를 사용해서 환경을 설정할 수도 있다. 이 포스팅은 Travis CI에서 ClearLinux docker를 사용한 설정에 대한 기록이다.

삽질1: Travis CI의 Ubuntu이용

빌드와 Google test를 이용한 unit test만 할 것이니까 OS를 크게 타지 않을테니 기본으로 제공되는 Ubuntu 환경에 필요한 도구들만 설치 하면 가장 빠르지 않을까?

일견 타당해 보이기는 하지만 문제는 의존성이다. Pre-compile된 Google test를 download 받는다 해도, 2019년 1월 현재 아직 Travis CI에서 제공하는 Ubuntu의 가장 최신 버전은 Xenial이다. CMake version이 안맞아서 최신버전으로 설치하고 Intel LibVA, Intel MediaSDK등의 의존 package들을 컴파일한 후 빌드를 하고 unittest를 하도록 하는데 14분이 넘게 걸렸다. 다음은 사용한 .travis.yml file이다.

language: cpp 

compiler:  - gcc 

dist: xenial 

env:   
  global:    
- EA_INSTALL_PREFIX=${TRAVIS_BUILD_DIR}/local    
- PATH=${EA_INSTALL_PREFIX}/bin:$PATH before_install:  
- mkdir -p ${TRAVIS_BUILD_DIR}/local  
- sudo apt-get install curl wget autoconf libtool libdrm-dev \
libboost-all-dev libgstreamer1.0-0 libasound-dev \
libgles2-mesa-dev gstreamer1.0-plugins-base \
gstreamer1.0-plugins-good gstreamer1.0-plugins-bad \
gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-doc \
gstreamer1.0-tools gstreamer1.0-x gstreamer1.0-alsa \
gstreamer1.0-pulseaudio
- cd ${TRAVIS_BUILD_DIR}&& \
wget https://github.com/Kitware/CMake/releases/download/v3.13.2\
/cmake-3.13.2.tar.gz \
&& tar xvf cmake-3.13.2.tar.gz&&cd cmake-3.13.2&&./configure --\
prefix=${EA_INSTALL_PREFIX}&&make&&make install  
- cd ${TRAVIS_BUILD_DIR}&& \
wget https://github.com/intel/libva/archive/2.3.0.tar.gz&&\
tar xvf 2.3.0.tar.gz \
&&cd libva-2.3.0&&./autogen.sh&&./configure --\
prefix=${EA_INSTALL_PREFIX}&&make&&make install  
- cd ${TRAVIS_BUILD_DIR}&& \
wget https://github.com/Intel-Media-SDK/MediaSDK/archive/\
intel-mediasdk-18.3.1.tar.gz \
&& tar xvf intel-mediasdk-18.3.1.tar.gz&&\
cd MediaSDK-intel-mediasdk-18.3.1/&& \
cmake -DCMAKE_INSTALL_PREFIX=/usr -DENABLE_OPENCL=OFF -DBUILD_SAMPLES=OFF .&&make&&\
make install 

script:  
- cd ${TRAVIS_BUILD_DIR} && \
cmake . && make && make install && test/ea_test

삽질2: Clear Linux docker image 사용 

시간만 오래 안 걸렸어도 기본 Ubuntu OS로 어떻게든 해보는 건데, 14분이면 시간이 너무 오래 걸린다. 이왕 시간이 오래 걸리는 거라면 타겟인 Clear Linux docker image를 사용해보자.

Clear Linux docker image를 생성하기 위한 dockerfile을 다음과 같이 작성해준 다음

FROM clearlinux

RUN clrtrust generate

RUN swupd bundle-add software-defined-cockpit-dev

.travis.yml file을 다음과 같이 선언해 준다.

language: cpp
services:
 - docker
before_install:
 - docker build -t clearlinux_ea .
 - docker run -d -v ${TRAVIS_BUILD_DIR}:/src clearlinux_ea /bin/sh -c "cd /src;cmake .;make;make install"

script:	 	 
 - docker run -d -v ${TRAVIS_BUILD_DIR}:/src clearlinux_ea /bin/sh -c "cd /src;test/ea_test"

총 소요된 시간은 17분 41초 그 중에 docker 설정하는데 걸린 시간만 16분이 넘는다. 나머지 시간에 unit test. 대부분의 시간이 docker를 빌드 하고 설정하는데 사용 되고 있었다. 

삽질3: 만들어 둔 Docker image 다운로드

빌드하는데 시간이 오래 걸린다면 이미 만들어 둔 docker image를 저장소에 넣어두고 pull해서 사용하면 좀 빠르지 않을까? Docker 빌드 vs Docker 다운로드.

이미 빌드 한 docker image를 공개 저장소인 docker hub에 넣어두고 Travis CI에서 pull하도록 변경하면 시간은 8분정도로 줄어든다.

language: cpp

services:
 - docker
	
before_install:
 - docker pull litcoder/clearlinux_ea
 - docker run -v ${TRAVIS_BUILD_DIR}:/src litcoder/clearlinux_ea /bin/sh -c "cd /src;cmake .;make;make install;"
	
script:
 - docker run -v ${TRAVIS_BUILD_DIR}:/src litcoder/clearlinux_ea /bin/sh -c "cd /src;test/ea_test"

흠.. 일단은 이걸로.

 결론

Travis CI에서 제공되는 연산 성능은 매우 떨어져서 컴파일이나 도커 빌드를 효율적으로 수행하지 못한다. 반면, 이미 만들어진 이미지의 다운로드는 상대적으로 빠르게 수행 할 수 있다. Travis CI에서 Docker를 이용한 테스트 환경을 구성하고자 한다면 미리 만들어 둔 이미지를 Docker Hub에 올려두고 CI script에서 pull 해서 사용하는 방법이 가장 고려해 볼 만한 선택이다.