으쌰으쌰 CI/CD 자동배포 - 정병재

TEAM.새싹·2020년 5월 8일
1
post-thumbnail

으쌰으쌰 프로젝트의 자동 배포를 구축하면서 학습 혹은 실습해보았던 것들을 정리해보았습니다.

CI (Continuous Integration)

개발자를 위한 자동화 프로세스

배포를 자동화하려면 배포가 진행되기 전에 테스트가 진행되어야 합니다. (너무나도 당연한 소리)

여러 사람이 동시에 애플리케이션을 개발하고 관련된 코드를 작업을 할 경우 충돌이 일어나거나 예기치 않는 문제가 발생할 수 있습니다. 이를 위해 개발자들은 안전장치로 Test Code를 작성하지만 여러 사람의 소스 코드를 통합할 때마다 혹은 변경사항이 있을 때마다 Test를 해야 합니다. 이러한 삽질을 자동화하기 위해 사용하는 것이 CI입니다.

앞서 말했듯이 CI(Continuous Integration)를 실행하기 위해서는 Test Code가 작성되야하면 Test 및 Build를 실행할 환경이 구축되어야 합니다. 하지만 이를 통해 얻는 것은 너무나도 당연하지만, 꼭 필요한 이득(예기치 않을 오류 방지)을 얻을 수 있습니다.

프로젝트 적용기

저희 으쌰으쌰 팀이 사용한 CI 툴을 Travis입니다.

Travis 를 선택한 이유

저희 팀은 Jenkins를 사용하고자 했지만, AWS에서 Jenkins와 Gradle Build를 같이 실행할 경우 Ram이 부족하여 사용하지 못하였습니다. 그래서 서버를 따로 구축하지 않고 사용할 수 있으며 Github 연동이 가능한 CI 툴인 Travis를 사용하기로 하였습니다.

작동 순서

  1. 개발

    개발단계에서는 담당개발자가 개발한 기능과 TestCode를 작성합니다.

  2. Github

    개발된 코드는 GitHub 소스코드에 저장되면 Pull Request나 Merge 같은 특정한 이벤트가 발생시 Travis에게 요청합니다.

  3. Travis

    Travis는 Github에 있는 소스코드를 바탕으로 Test 및 Build를 실행 시켜봄으로 써 소스코드에 오류 혹은 시스템 상에 문제가 없는 지 체크합니다.

CD ( Continous Delivery / Deployment)

지속적인 전달 / 배포

Test 자동화 만으로도 충분하지 않을까? 그렇지 않습니다. 사람의 욕심은 끝이 없어요. CI를 거친 후 배포 혹은 전달 같은 과정을 자동화 할 수 있는 프로세스가 필요합니다. 이를 CD (Continuous Delivery / Deployment)라고 합니다.

Delivery / Deployment 두 단어를 사용한 의미를 파악하면 무슨 일을 하는지 알수 있습니다.

Delivery

CI의 빌드 자동화, 유닛 및 통합 테스트가 실행된 이후에 실행되는 과정입니다. 이 프로세스는 CI 과정에서 유효한 코드를 바탕으로 릴리스합니다. 즉, 쉽게 말해 테스트 통과된 소스 코드를 확보하여 프로덕션 환경으로 배포할 준비를 한다는 말입니다.

Deployment

Deployment는 마지막 단계이자 CI / CD 자동화의 목적입니다. 말 그대로 배포입니다. Delivery 과정에서 준비된 코드를 자동으로 배포할 수 있도록 하며 모든 과정으로 자동화함으로 그만큼 위험이 따릅니다.

[출처 : BitBucket]

위 그림은 Delivery와 Deployment의 차이를 명확히 보여줍니다.

Contiuous delivery는 개발 준비 단계까지 실행하며 배포는 직접합니다. 그렇기에 불안전한 기능이 있더라도 프로덕트에 반영되지 않기 때문에 비교적 안전합니다. 하지만 Continous deployment는 CI부터 배포까지 모든 과정을 자동화시키기 때문에 위험성이 따릅니다. 그럼에도 Continous deployment를 사용하는 이유는 자동화함으로써 얻을 수 있는 것이 많기 때문입니다. Test와 배포과정에 문제가 없다면 오류가 있든지 작은 변경사항을 빠르게 반영시킬 수 있 수 있기에 많이 사용합니다.

프로젝트 적용기

윗글에서 저희 팀은 CI 툴을 Travis 툴을 사용한다고 언급하였습니다.

Travis는 .travis.yml파일에 CI/CD 과정 혹은 설정을 명시할 수 있게 하였습니다. 다음은 저희 팀의 .travis.yml파일입니다.

language: java
jdk:
- openjdk11
branches:
  only:
  - master

# Travis CI 서버의 Cache 활성화
cache:
  directories:
  - "$HOME/.m2/repository"
  - "$HOME/.gradle"

# gradle build
script: "./gradlew clean build"

# slack notification
notifications:
  slack:
    secure: ...
before_deploy:
  - zip -r eussya-eussya-api *
  - mkdir -p deploy
  - mv eussya-eussya-api.zip deploy/eussya-eussya-api.zip

after_success:
  - ./gradlew jacocoTestReport coveralls -Djdk.tls.client.protocols="TLSv1,TLSv1.1,TLSv1.2"

# aws code deploy
deploy:
  - provider: s3
    access_key_id: $AWS_ACCESS_KEY # Travis repo settings에 설정된 값
    secret_access_key: $AWS_SECRET_KEY # Travis repo settings에 설정된 값
    bucket: eussya-eussya-api #  S3 버킷
    local_dir: deploy # before_deploy에서 생성한 디렉토리
    region: ap-northeast-2
    skip_cleanup: true
    acl: public_read
    wait_until_deployed: true
    on:
      repo: sproutt/eussya-eussya-api #Github 주소
      branch: master

  - provider: codedeploy
    access_key_id: $AWS_ACCESS_KEY # Travis repo settings에 설정된 값
    secret_access_key: $AWS_SECRET_KEY # Travis repo settings에 설정된 값
    bucket: eussya-eussya-api # S3 버킷
    key: eussya-eussya-api.zip # S3 버킷에 저장된 springboot-webservice.zip 파일을 EC2로 배포
    bundle_type: zip
    application: eussya-eussya-api # 웹 콘솔에서 등록한 CodeDeploy 어플리케이션
    deployment_group: sproutt # 웹 콘솔에서 등록한 CodeDeploy 배포 그룹
    region: ap-northeast-2
    wait_until_deployed: true
    on:
      repo: sproutt/eussya-eussya-api #Github 주소
      branch: master

저희 팀은 Github의 master 브랜치를 기준으로 하며 merge 혹은 pull request 요청 시 CI를 실행시킵니다. 그 이후 과정은 다음과 같습니다.

  1. script: "./gradlew clean build"

    gradle build를 실행하여 빌드 시 문제가 있는지 없는지 판단합니다.

  2. 빌드 이후 후 이벤트

    • Coveralls : Test coverage를 Github Readme에 시각적으로 표시할 수 있도록 합니다.
    • Slack : 슬랙에 빌드의 성공 여부를 알립니다.
  3. AWS Deploy

    • S3 : AWS에 빌드된 파일을 저장합니다. 앞서 말했듯이 Test 과정의 이후 단계이며 Delivery 단계로써 배포 가능한 파일들이 담기게 됩니다.

    • Codedeploy : AWS EC2에서 S3에 담긴 배포 가능할 파일들을 가지고 자동으로 배포할 수 있도록 합니다.

작동 순서

마치며

지금까지 CI / CD의 개념을 바탕으로 으쌰으쌰 프로젝트에 어떻게 적용시켰는지 알아보았습니다. 사실 개념상으로 CI / CD를 구축하는 것은 어렵지 않았지만 환경변수 설정이나 리눅스 접근 권한 같이 구축과정에서 발생한 문제들를 해결하는 것이 어려웠습니다. 이러한 부분은 어떻게 해결해 나아갔는지 정리해서 에 저장해 놓도록하겠습니다. 감사합니다.

부록

1. Jenkins와 Docker 삽질기 (으쌰으쌰 프로젝트)

EC2 with jenkins

  1. 시작하기 앞서 sudo apt update 명령어를 이용하여 apt 업데이트하기

  2. Docker 를 설치한다. sudo apt install docker.io

  3. systemctl 설정

    sudo systemctl start docker
    sudo systemctl enable docker

  4. docker를 통해 jenkins 컨테이너 받아오기

    sudo docker pull jenkins

  5. jenkins 이미지를 사용한다

    sudo docker run -d -p 8080:8080 -v /home/jenkins:/var/jenkins_home jenkins

  6. jenkins 8080포트로 접속하여 설정한다.

  7. jenkins 컨테이너 접속 docker exec -it c7d2c6d8bb28 /bin/bash

  8. jenkins 내부에 docker build 설치하기

    curl -s httpS://get.docker.com/ | sudo sh

--> plugin error 이유 : jenkins 컨테이너로 받을 때는 버전이 낮아서 plugin 설치가 안됨.

해결법

  1. 젠킨스 컨테이너에 접속 docker container exec -u 0 -it jenkins bash

  2. update tool down wget http://updates.jenkins-ci.org/download/war/2.89.2/jenkins.war

  3. download 파일 옮기기mv ./jenkins.war /usr/share/jenkins

  4. update chown jenkins:jenkins /usr/share/jenkins/jenkins.war (updated)

  5. restart

    # exit contaienr (inside container)
    exit# restart container (from your server)
    docker container restart jenkins

  1. 컨테이너가 아닌 jenkins 직접설치

    sudo apt update
    sudo apt install openjdk-8-jdk
    
    wget -q -O - https://pkg.jenkins.io/debian/jenkins.io.key | sudo apt-key add -
    
    sudo sh -c 'echo deb http://pkg.jenkins.io/debian-stable binary/ > /etc/apt/sources.list.d/jenkins.list'
    
    sudo apt update
    sudo apt install jenkins
    
    systemctl status jenkins
  2. 8080포트로 접속하여 세팅하기

  3. jenkins

    • Git repository URL
    • GiHub project URL
    • GIt WebHook
  4. sudo err -> https://gist.github.com/hayderimran7/9246dd195f785cf4783d

  5. shell script 작성

문제

./gradlew 로 build 실행 시 무한루프에 걸림

  • 오류 찾지 못함

jenkins 내gradle wrapper 사용시

  • build 안됨
  • gradle -v 체크해보기

시도한 내용

jenkins 도커 컨테이너로 받아서 2.7 이상 버전으로 업데이트하기 -> ec2 권한 문제 해결 
공식 document에서 제공하는 모든 script 사용
Gradle version 설정 (어떤 오류 해결 -> 또다른 오류 등장)
test 에서 멈추서 jnunit 등 build 할때 필요한 의존성 추가 하고 시도
ec2 권한 설정 
local 상에 jenkins 실행 
ec2 ssh 접속 (local 상에 build 실행 실패 -> java 11 필요함.)
java 11 지원하는 jenkins 도커 생성(진행중)
jenkins caches 삭제
  • 결론 : java 11 버전, gradle 버전 호환성 문제

  • java 11 설정

    docker pull jenkins/jenkins:jdk11
    docker run --rm -ti \
      -p 8080:8080 -p 50000:50000
      -v jenkins-home:/var/jenkins_home \
      jenkins/jenkins:jdk11

결론

  • java와 gradle 버전을 맞게 설정 (java 11버전 찾아서 jenkins 받기)

  • 메모리가 0.5g 이면 memory 실패

  • 메모리가 1g이면 build도 중 메모리 부족으로 무응답 상태가 됨 (사용하는 ec2)

  • 메모리가 2g가 이상 필요함 ㅠㅠ

Docker

SprinbBoot DockerFile 만들기

FROM openjdk:8-jdk-alpine
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

출처 : spring.io - https://spring.io/guides/gs/spring-boot-docker/
# Start with a base image containing Java runtime
FROM java:8

# Add Author info
LABEL maintainer="f.softwareengineer@gmail.com"

# Add a volume to /tmp
VOLUME /tmp

# Make port 8080 available to the world outside this container
EXPOSE 8080

# The application's jar file
ARG JAR_FILE=build/libs/MySpringApp-0.0.1-SNAPSHOT.jar

# Add the application's jar to the container
ADD ${JAR_FILE} to-do-springboot.jar

# Run the jar file
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/to-do-springboot.jar"]

출처: https://imasoftwareengineer.tistory.com/40 [삐멜 소프트웨어 엔지니어]
  • FROM : 이미지를 만들기위해 기반이 되는 이미지 레이어
    • From <이미지 이름> : <태그>
  • LABEL : 이미지를 관리하는 사람이 누구인지 명시
  • VOLUME : 컨테이너가 필요한 여러가지 데이터를 저장하는 곳
  • EXPOSE : 포트 넘버
  • ARG : 어떤 어플리케이션을 실행하는지, 즉 어플리케이션으 실행파일은 연결
  • ADD${JAR_FILE} : 이름을 붙여준다.
  • ENTRYPOINT : 실행시키기 위한 명령어

으쌰 프로젝트에 적용

  • 첫번째 DockerFile을 적용

  • docker build --build-arg JAR_FILE=build/libs/*.jar -t sproutt/eussya-eussya-api .
    docker run -p 8080:8080 -t <TagName>

참고

https://imasoftwareengineer.tistory.com/40

https://spring.io/guides/gs/spring-boot-docker/




2. Travis CI / Coveralls / aws code deploy 구축

https://jojoldu.tistory.com/275

Travis

  1. travis가입 후 레파지토리 연결

  2. .travis.yml 파일 생성

    language: java
    jdk:
      - openjdk11
    
    branches:
      only:
        - master
    
    # Travis CI 서버의 Cache 활성화
    cache:
      directories:
        - '$HOME/.m2/repository'
        - '$HOME/.gradle'
    
    # clean 후 Build (Build시 자동으로 test 수행)
    script: "./gradlew clean build"
  3. Readme.md에 build결과 보이도록 표시하기

  4. slack notification 연동

설정

  • 접근 권한 문제

    # 실행전 접근 권한 설정
    before_install:
      - chmod +x gradlew
  • slack notification

    travis 암호화 기능을 활용하여 key 값보이지 않게하기 / travis 세팅하는 곳에서 key값 정의 해놓기

발생한 문제

  • Application yml 에 설정된 secret key 같은 민간함 정보를 담을 때 git에 올릴수 없고 그로 인해 발생된 travis build 실패를 방지하기 위한 조치

    • email : ${MAIL_EMAIL} //application.yml
      • MAIL_EMAIL를 환경변수로 지정하기(travis)

Coveralls

  1. coverall 가입 후 연동

  2. .coveralls.yml 파일생성 후 repo_token 값 설정하기

  3. coverage build를 위한 build.gradle 설정

    buildscript {
        ...
        dependencies {
            ...
            classpath 'org.kt3k.gradle.plugin:coveralls-gradle-plugin:2.8.2'
        }
    }
    
    apply plugin: 'com.github.kt3k.coveralls'
    apply plugin: 'jacoco'
    
    ...
    
    jacocoTestReport {
        reports {
            xml.enabled = true // coveralls plugin depends on xml format report
            html.enabled = true
        }
    }
    
    ...
  4. .travis.yml 빌드 성공 후 실행하도록 설정

    after_success:
    - ./gradlew jacocoTestReport coveralls
  5. slack notification - incoming webhook 활용하기

aws code deploy 구축

https://jojoldu.tistory.com/265 참고하기

문제

  • please upgrade for ruby... 문제

    => ec2 instance 와 s3 tag 네임 잘 설정하기 + ec2 배포 지역 이름 통일화하기 ex) northeast-a

  • Oops, It looks like you tried to write to a bucket that isn't yours or doesn't exist yet. Please create the bucket before trying to write to it. 문제

    => 새 퍼블릭 ACL 및 퍼블릭 객체 업로드 차단만 체크해제

  • ec2 java 11 설치 (aws docs)




3. 스프링 부트 실행시 환경 변수 설정 안되는 문제(ec2)

java jar 로 실행하면 환경변수가 적용되는 데 .sh 를 실행하면 왜 안될까?

원인 분석

  • 실행하고자 하는 shell script의 접근 권한
    • chmod를 활용하여 최고 단계의 접근 권한을 줘도 안됨
  • --server.port 옵션을 붙여서 그러지 않을 까?
    • 없애도 안됨
  • 실행하는 스프링 부트의 경로가 절대경로가 아닌 상대경로로 지정하면 어떨까?
    • 안됨.
  • sudo를 이용해서 실행하는 데 이것때문일까?
    • O

왜 sudo를 이용하면 안될까?

다음은 환경변수 목록과 sudo를 이용한 환경변수 목록이다

sudo를 이용할 경우 환경변수 목록이 현저히 줄어드는 것을 확인 할 수 있다.

그렇다면 왜! 환경변수가 적용 되지 않을 걸까?

  • Linux의 기본 보안 정책 플러그인으로 인해 제한됬기 때문이다

해결방법

https://unix.stackexchange.com/questions/337819/how-to-export-variable-for-use-with-sudo

sudo 를 활용해서 실행할 때 sudo 앞에 환경변수를 설정하고 -E옵션을 사용한다

<Environment Variables>=<value> sudo -E ...

Ex) JASYPT_PASSWORD=sprouttsecret sudo -E java -jar $REPOSITORY/jar/$JAR_NAME --server.port=80

profile
전남대 출신 개발 스터디 그룹 새싹팀 입니다. @ECONOVATION

2개의 댓글

comment-user-thumbnail
2020년 5월 9일

오호 꿀 정보 감사합니다~

답글 달기
comment-user-thumbnail
2020년 5월 9일

CI 에 대한 좀 더 자세한 내용은
https://en.wikipedia.org/wiki/Continuous_integration
를 참고하시면 좋을 것 같습니다

답글 달기