본문 바로가기
Work/개발 노트

하이브 런칭기 #1 - 사내 환경 구성

by ★용호★ 2018. 8. 15.

초기 개발환경

처음에 사내 개발환경은 위키에 작성된 매뉴얼에 따라 물리 머신에 개발에 필요한 각 프로그램을 다운받고 설치하여 구성을 하였습니다. 개발을 진행하면서 새로운 머신에 개발환경을 셋팅하는 일은 드물게 발생하진 않지만 새로 설치할 때마다 위키 매뉴얼을 참조하며 한땀한땀 환경을 구축해나가야 합니다. 매뉴얼 대로 문제없이 진행이 되면 좋겠지만 대부분의 경우에 구축할 때마다 어디선가 오류가 발생합니다. 이전에 봤던 오류였는데 메모를 제대로 해놓지 않아서 다시 구글링을 하기도 하고, 같은 과정을 반복하며, 환경 설정에만 하루 이틀의 시간을 낭비하며 보내게 됩니다. 환경을 구성하는 여러 프로그램들 중에서 버전업을 한다거나 설정을 변경해야하는 일이 발생한다면 그 때마다 위키 매뉴얼을 수정해주어야 하고 팀원들에게 공유도 해야하기 때문에 설정 변경에도 많은 시간이 소모되기도 합니다.

Docker 도입

그래서 항상 동일한 환경을 구축할 수 있도록 하기 위해 docker를 도입하기로 결정하였습니다. docker 이미지를 생성하기 위한 Dockerfile을 정의하면, 위키 매뉴얼도 필요 없이 모든 팀원들이 어떻게 환경이 구셩되었는지 이해할 수가 있습니다. 전부 개발자들이기 때문에 오히려 문서로 하나하나 설명한 것보다 Dockerfile에 기록된 스크립트가 보기에 명확하고 이해도 빨랐습니다. 초기에는 ubuntu 이미지에 필요한 패키지들을 설치해서 사용하는 방식으로 사용했지만 이미지 빌드 시간이 너무 오래걸리고, 불필요한 패키지들이 포함되기 때문에 각 서비스 별로 기본 이미지를 사용하는 방식을 사용하였습니다. http://hub.docker.com페이지에서 사용할 서비스를 검색한 후 official로 등록된 이미지들을 최우선으로 사용하였습니다. 예를 들어 tomcat의 경우에는 아래 이미지와 같이 tomcat으로 검색한 후 오른쪽 상단의 드롭다운 버튼을 클릭한 후 Official을 선택하면 공식 이미지 리스트가 출력됩니다.

여기서 DETAILS 버튼을 클릭하고 들어가면 해당 이미지에 대한 다양한 정보와 명령어에 대한 예시들도 포함되어 있기 때문에 손쉽게 사용해볼 수가 있습니다. 어떻게 구성되었는지 확인하고 싶은 경우에는 아래와 같이 각 버전별로 제공되는 Dockerfile 링크를 타고 들어가면 Github 레파지토리에서 Dockerfile 내용을 볼 수도 있습니다.

이 tomcat 이미지를 그대로 사용해도 되지만 언어 설정이나 ssh 접속을 위한 openssh 설치와 같이 추가적으로 구성해야 될 패키지들이 있을 수 있습니다. 저 또한 필요한 패키지들이 점점 추가되었기 때문에 그 때마다 Dockerfile에 갱신해서 다음번에 환경을 구성할 때도 패키지를 빼먹는 일 없이 동일한 환경으로 구성할 수가 있었습니다. 아래와 같이 기본 이미지를 FROM에 명시하고, 추가적인 내용들을 아래에 정의하였습니다.

  • Dockerfile 예시

    FROM tomcat:8.0-jre8
    MAINTAINER Server Team <yongho1037@vinusent.com>
    
    RUN apt-get clean && apt-get update -y && apt-get install -y openssh-server locales vim sudo
    
    # SSH 관련 설정 (사용자 계정 생성 및 sudo 권한 부여)
    RUN adduser --disabled-password --gecos "" hive  \
        && echo 'hive:' | chpasswd \
        && adduser hive sudo \
        && echo 'hive ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers \
        && mkdir /var/run/sshd
    
    # 시간 설정
    RUN rm /etc/localtime && ln -s /usr/share/zoneinfo/UTC /etc/localtime
    
    # 언어 설정
    RUN sed -i -e 's/# ko_KR.UTF-8 UTF-8/ko_KR.UTF-8 UTF-8/' /etc/locale.gen && \
        echo 'LANG="ko_KR.UTF-8 UTF-8"'>/etc/default/locale && \
        dpkg-reconfigure --frontend=noninteractive locales && \
        echo 'export LANG=ko_KR.utf-8' >> /etc/bash.bashrc
    
    # filebeat 설치
    RUN curl -L -O https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-5.3.0-amd64.deb
    RUN dpkg -i filebeat-5.3.0-amd64.deb
    RUN rm filebeat-5.3.0-amd64.deb
    
    # 설정 파일 복사
    COPY conf/catalina.sh /usr/local/tomcat/bin/
    COPY conf/filebeat.yml /etc/filebeat/
    COPY conf/server.xml /usr/local/tomcat/conf/
    
    RUN chmod go-w /etc/filebeat/filebeat.yml
    
    CMD ["/usr/sbin/sshd", "-D"]
    
    • 사내에서 배포를 위해 jenkins를 사용하고 있는데 jenkins를 통해 war 파일을 배포하고 tomcat을 재시작하게 되면 tomcat 프로세스가 내려감과 동시에 컨테이너가 종료되기 때문에 이를 방지하기 위해 CMD 명령으로 sshd를 실행하도록 설정하였습니다.

    • CMD 명령은 컨테이너가 구동 될 때 실행되는 스크립트 또는 실행 파일을 의미하는데 여기서 실행된 프로세스가 PID 1번이 됩니다. 리눅스 커널에 의해 시작된 첫번째 프로세스가 PID 1을 얻게 되는데 이 프로세스가 종료되면 다른 프로세스들도 KILL 신호로 종료되어 결국 컨테이너가 종료됩니다.

      UID        PID  PPID  C STIME TTY          TIME CMD
      root         1     0 30 20:05 ?        00:00:03 /docker-java-home/jre/bin/java -
      root        53     0  1 20:05 pts/0    00:00:00 bash
      root        57    53  0 20:05 pts/0    00:00:00 ps -ef
      

사내에서 사용되는 모든 서비스들을 Docker로 구성하다보니 Dockerfile이 점점 많아지게 되어 현재는 아래와 같은 디렉토리 구조가 구성되었습니다. 매번 Dockerfile로 빌드를 해서 사용하지는 않지만 Dockerfile이 변경된 히스토리나 구성된 내용을 확인하기 위해서 하나의 디렉토리에 각 서비스를 위한 파일들을 비슷한 디렉토리 구조 하위에 파일들을 관리하고 있습니다.

  • Dockerfile을 모아둔 디렉토리의 구조

    ├── elasticsearch
    │   ├── Dockerfile
    │   └── conf
    │       ├── elasticsearch.yml
    │       ├── jvm.options
    │       └── sysctl.conf
    ├── httpd
    │   ├── Dockerfile
    │   ├── conf
    │   │   ├── httpd.conf
    │   │   └── workers.properties
    │   └── modules
    │       └── tomcat-connectors-1.2.42-src.tar.gz
    ├── jenkins
    │   ├── Dockerfile
    │   └── script
    │       └── init.sh
    ├── kibana
    │   ├── Dockerfile
    │   ├── conf
    │   │   ├── kibana.yml
    │   │   └── logtrail.json
    │   └── dashboard
    │       └── export.json
    ├── kibana-nginx
    │   ├── Dockerfile
    │   ├── conf
    │   │   ├── kibana-admin.conf
    │   │   └── kibana-readonly.conf
    │   └── start.sh
    ├── logstash
    │   ├── Dockerfile
    │   └── conf
    │       ├── logstash.conf
    │       └── logstash.yml
    ├── mariadb
    │   ├── Dockerfile
    │   ├── README
    │   ├── conf.d
    │   │   ├── mariadb.cnf
    │   │   ├── mysqld_safe_syslog.cnf
    │   │   └── tokudb.cnf
    │   └── sql
    │       ├── init.sql
    │       └── truncate.sql
    ├── mongo
    │   ├── Dockerfile
    │   ├── README
    │   ├── conf
    │   │   ├── disable-transparent-hugepages
    │   │   └── mongod.conf
    │   └── run.sh
    ├── nginx
    │   ├── default.conf
    │   └── ssl
    │       ├── nginx.crt
    │       └── nginx.key
    ├── pinpoint-collector
    │   ├── Dockerfile
    │   └── conf
    │       ├── hbase.properties
    │       └── pinpoint-collector.properties
    ├── pinpoint-hbase
    │   ├── Dockerfile
    │   ├── conf
    │   │   ├── hbase-env.sh
    │   │   └── hbase-site.xml
    │   └── start.sh
    ├── pinpoint-web
    │   ├── Dockerfile
    │   └── conf
    │       ├── hbase.properties
    │       └── pinpoint-web.properties
    ├── redis
    │   ├── Dockerfile
    │   └── conf
    │       └── redis.conf
    ├── registry
    │   ├── 1.run-registry.sh
    │   ├── 2.run-nginx.sh
    │   ├── conf
    │   │   └── nginx.conf
    │   └── ssl
    │       ├── server.crt
    │       ├── server.csr
    │       └── server.key
    └── tomcat
        ├── Dockerfile
        ├── conf
        │   ├── catalina.sh
        │   ├── filebeat.yml
        │   └── server.xml
        └── start.sh
    

Repository 구성

위와 같이 프로젝트에 필요한 각종 서비스들을 Dockerfile로 작성 후 이미지화 해서 사용하기 시작했는데, 팀원들도 로컬에 구성을 하기 위해서 각각 Dockerfile로 빌드하여 사용하다보니 docker에 대해 잘 모르는 팀원들에게는 절차를 알려줘야 하기도 하고, 버전이 조금씩 다른 이미지를 사용하게 되기도 해서 사내에 private repository를 구성하게 되었습니다. Repository를 사용하게 되면 이미지를 간단하게 pull 받아서 사용할 수 있기 때문에 다른 팀원들도 Dockerfile을 빌드해야 하는 번거로움이 없어지고 변경사항이 발생하더라도 commit 명령과 push를 통해서 간편하게 이미지 업데이트가 가능하기 때문에 편리했습니다.

나중에는 아마존 웹 서비스를 사용하면서 서비스 중 하나인 ECR(Elastic Container Repository)를 사용하여 AWS 상의 Repository에 이미지를 push하여 Repository에 대한 관리 부담도 덜어낼 수 있었습니다.

docker-compose 구성

각 컨테이너들 간의 통신이 필요한 경우에는 --links 옵션을 사용하여 컨테이너들 간에 통신이 가능하도록 설정을 해서 사용을 하였습니다. 하지만 각 컨테이너들을 생성할 때마다 docker run 명령을 통해 볼륨지정과 포트 지정, 링크 지정 등 옵션들을 길게 나열해서 작성하다보니 이 또한 번거로운 작업이 되었습니다. 그리고 각 컨테이너들간의 관계도 기억하고 있어야 한다는 것 또한 불편하게 느껴졌습니다.

그래서 이러한 과정도 한번 정의하면 그대로 사용할 수 있도록 docker-compose를 사용하여 구성을 하였습니다. docker-compose를 사용하면 각 컨테이너들 간의 통신이나 볼륨지정, 포트 포워딩 등 다양한 옵션 설정을 파일에 기록하여 더이상 신경쓰지 않아도 되었기 때문에 편하게 느껴졌습니다. 이로 인해 새로운 환경을 구축할 때도 OS 설치 시간과 docker를 설치하는 시간을 제외하면 10분 내로 환경 구성이 가능해졌습니다.

  • Docker-compose 예시

    version: '2'
    services:
      redis:
        image: 1234.dkr.ecr.ap-northeast-1.amazonaws.com/hive-redis:latest
        restart: always
        volumes:
         - ~/service/log/redis:/var/log/redis
         - ~/service/data/redis:/data
        ports:
         - "6379:6379"
      mariadb:
        image: 1234.dkr.ecr.ap-northeast-1.amazonaws.com/hive-mariadb:latest
        restart: always
        environment:
         - MYSQL_ROOT_PASSWORD=root
        volumes:
         - ~/service/log/mariadb:/var/log/mysql
         - ~/service/data/mariadb:/var/lib/mysql
        ports:
         - "3306:3306"
      tomcat:
        image: 1234.dkr.ecr.ap-northeast-1.amazonaws.com/hive-tomcat:latest
        restart: always
        links:
         - mariadb
         - redis
         - redis_event
        volumes:
         - ~/service/log/server-1:/usr/local/tomcat/logs
        ports:
         - "8080:8080"
         - "9122:22"
      httpd:
        image: 1234.dkr.ecr.ap-northeast-1.amazonaws.com/hive-httpd:latest
        restart: always
        volumes:
         - ../httpd/conf/httpd.conf:/usr/local/apache2/conf/httpd.conf
         - ../httpd/conf/workers.properties:/usr/local/apache2/conf/workers.properties
         - ~/service/log/httpd:/usr/local/apache2/logs
        network_mode: "host"
    • httpd의 경우 외부로부터 패킷을 받는 진입점이므로 network_mode를 host로 지정하였습니다. host로 지정하지 않으면 내부적으로 docker proxy를 사용하기 때문에 약간의 지연이 발생하게 됩니다.
    • tomcat의 경우 jenkins와 ssh 통신을 하기 위해 22번 포트를 임의의 포트(위에서는 9122)로 포트포워딩 설정 하였습니다.

빌드 및 배포

하이브 프로젝트를 진행하면서 개발환경에서부터 라이브환경까지 각종 서버들과 툴들에 대한 빌드 및 배포를 젠킨스에서 관리하도록 구성하였습니다. 빌드는 커밋된 내용이 있는 경우 주기적으로 자동 빌드를 하도록 설정을 해두었고, 배포가 필요할 때는 해당 서버로 원클릭 배포가 가능하도록 구성해두었습니다. 

서버에서 제작하는 모든 프로젝트는 Subversion을 통해 소스 코드가 관리되고 있고, gradle을 사용하여 빌드하고 있습니다. 그래서 jenkins를 통해 빌드 시 먼저 아래와 같이 Subversion을 통해 소스 코드를 갱신합니다. 

그리고 아래와 같이 Gradle 플러그인을 사용하여 빌드를 수행합니다. 

Tasks 항목에 build를 입력하면 빌드와 더불어 테스트 코드까지 함께 수행되어서 매 빌드 시 자동적으로 회귀 테스트를 수행 할 수 있기 때문에 코드의 안정성을 유지할 수가 있습니다. 하지만 프로젝트를 진행하다보니 테스트 코드를 항상 최신으로 유지하는 것이 쉽지 않은 일이라는 것을 깨달았고, 빠듯한 일정으로 인해 팀원들이 모두 테스트를 신경쓸 수 없는 지경에 이르게 되었습니다. 그래서 팀 내 협의를 통해 구현 시 반드시 테스트 코드는 작성하되 오래된 테스트 코드의 관리는 빡빡하게 하지 말자는 결론이 나왔고 결과적으로 빌드 시 테스트는 제외하도록 설정하게 되었습니다. 그래서 빌드 옵션에 -x test를 추가하여 테스트 수행은 제외시켰습니다. 라이브 환경에서는 Amazon Elastic Beanstalk를 사용하기 때문에 배포를 위해 war을 업로드하는 절차가 필요합니다. 이를 위해 war파일을 S3에 업로드하고, Beanstalk 배포 시에 해당 S3 경로에서 war 파일을 사용하는 방식으로 배포를 진행하고 있습니다. 그래서 아래와 같이 빌드가 완료되면 생성된 war 파일을 S3로 업로드 합니다. (참고로 aws 커맨드를 사용하기 위해서는 jenkins가 설치된 곳에 AWSCLI를 설치해야 하고 credential 설정이 되어 있어야합니다.) 

마지막으로 아래 그림과 같이 Archive the artifacts 플러그인을 통해 생성된 war파일을 아카이빙 합니다. 이렇게 하면 추후 다른 jenkins Item에서 이 Item을 참조하여 생성된 war파일을 손쉽게 가져다 사용할 수가 있습니다. 이를 이용해서 Amazon Elastic Beanstalk가 아닌 war파일을 직접 tomcat에 복사하여 구동시켜야 하는 서버들에 배포를 수행하고 있습니다.

빌드가 완성되고 나면 이제 배포를 수행해야합니다. 배포 시에는 대부분 최근 빌드를 배포하게 되지만 필요에 따라 이전 빌드를 배포해야하는 경우도 생깁니다.(예를 들어 최근 빌드에서 에러가 발생하여 급하게 이전 빌드로 되돌려야 하는 경우) 그래서 배포 시 build를 통해 생성된 Artifact를 선택할 수 있도록 아래와 같이 Build selector for Copy Artifact 매개변수를 사용합니다. 

이렇게 설정하고나서 배포 Item의 빌드 버튼을 클릭하면 아래와 같이 Artifact를 선택할 수 있는 페이지가 출력됩니다. 최근 빌드를 선택할 수도 있고, Artifact의 빌드 번호를 지정할 수도 있습니다. 

이제 어떠한 Artifact를 사용할지 선택했으니 해당 Artifact를 어디에서 참조할지 선택할 차례입니다. 그래서 Build 항목에 Copy artifacts from another project 플러그인을 사용하여 빌드를 수행했던 프로젝트를 지정하고 Parameter Name에는 앞서 선택했던 매개변수를 지정합니다. 

마지막으로 지정된 Artifact를 원격지에 있는 웹 서버로 전송하고 해당 war파일을 tomcat 서버에 적용한 후 재시작 하는 절차를 스크립트로 작성해야 합니다. 빌드를 수행했던 프로젝트에서 생성된 artifact를 ssh로 전달하기 위해 Source files 항목에 artifact의 경로를 선택하고 Remote directory에 디렉토리를 지정하면 해당 디렉토리로 Artifact가 복사됩니다.(참고로 Remote directory는 Jenkins 환경 설정에서 SSH Server 설정 시 지정한 Path를 기준으로 한 상대경로입니다.) 아래 그림에서는 스크립트 작성 부분은 생략하였는데, 이 부분에 ssh로 전달된 war 파일을 tomcat의 webapps 디렉토리 하위로 복사한 후 tomcat을 재시작하는 스크립트를 작성하면 됩니다. 

지금까지 설명한 것은 원격지 서버로 배포하는 절차였습니다. Amazon Elastic Beanstalk의 경우에는 SSH를 통해 파일을 복사하고 재구동하는 절차 필요 없이 AWSCLI를 사용하여 더욱 손쉽게 배포를 수행할 수가 있습니다. 앞서 빌드 시에 생성된 Artifact를 S3로 업로드하는 부분이 있었는데 여기서 이를 사용하게 됩니다. 아래 그림과 같이 먼저 S3에 업로드 된 Artifact를 Amazon Elastic Beanstalk의 Application Version으로 업로드하고, 업로드된 war파일을 배포합니다. (그림에서 커맨드라인이 길어서 일부 생략했습니다.) 

개발을 진행하면서 기획팀에서 가장 곤란해 했던 부분이 갱신된 테이블을 서버에 반영하는 것이었습니다. 기존에는 반영할 때마다 기획팀에서 서버팀에 부탁을 해야했기 때문에 작업자들이 매번 부탁하기를 미안해했었습니다. 그래서 이로 인해 공격적인 테스트가 불가능 했기 때문에 결과적으로는 개발 속도에 영향을 미치게 되었고 버그를 제때 발견하지 못해 더 큰 문제로 이어지기도 했었습니다. 그래서 기획팀에서 자유롭게 서버에 반영을 할 수 있도록 jenkins를 통해 제공을 하였습니다. 이 과정에서 실수로 다른 프로젝트의 빌드를 수행하거나 배포를 할 수 있기 때문에 아래와 같이 권한 관리를 추가하게 되었습니다. 

이렇게 개발환경이든 라이브환경이든 jenkins를 통해서 자동화한 덕분에 수시로 빌드 및 배포가 가능했고 문제가 생기더라도 언제든 이전 버전으로 돌릴 수 있어서 편리하였습니다.

마무리

사내 테스트 환경의 아키텍처는 아래와 같습니다.

Docker를 통해서 간편하게 환경을 구성할 수 있었고, 테스트를 진행하는 도중에 컨테이너에 문제가 생길 경우에도 컨테이너를 제거하고 다시 생성하는 절차도 빠르고 간단하기 때문에 개발 환경에 대해서는 더이상 신경쓰지 않고 개발에만 집중 할 수 있게되는 장점이 있었습니다. 데이터나 로그파일과 같은 경우에는 볼륨을 지정하여 사용하기 때문에 이미지에 대한 업데이트가 발생하더라도 컨테이너 재생성 시에도 개발에는 지장이 없도록 구성을 하였습니다.




같이 보면 좋은 포스팅


댓글