Backend/CI CD

Github Actions, Docker, EC2를 이용한 CI/CD 구현하기

anxi 2024. 4. 7. 00:52

근황

https://developer-anxi.tistory.com/27

 

[계획] 2024.03 ~ 2024.06

1. 자바 ORM 표준 JPA 프로그래밍 책 끝내기. - 방학 동안 끝내려고 했으나 ... 방학 때 다른 길에 빠져 공부에 소홀했다. 그래서 1학기 종강 전까지 책을 끝낼 예정이다 !!! 2. 코딩테스트 합격자 되기

developer-anxi.tistory.com

 

이글 쓴지 거의 한달이 다 되어가네요,,

매주 3개 이상 블로그 글 작성을 다짐했는데 이뤄지지 않았습니다,,

 

변명을 하자면 저는 현재 동아리에서 팀 프로젝트와 개인 프로젝트를 진행하고 있습니다.

근데 팀 프로젝트에서 이슈가 하나 생겨서 그걸 해결하려다 시간이 벌써 이렇게 되버렸네요,,

차차 이 내용은 블로그에 정리해서 기록하도록 하겠습니다.


CI (Continuous Integration)

 

CI는 지속적인 통합을 의미합니다.

 

즉, 새로운 코드의 변경 사항이 빌드 및 테스트를 통해 공유 레포지토리에 통합(INTEGRATE)되는 것을 의미합니다.

CI가 단순한 소스 코드의 빌드 및 배포만 의미하지 않고 테스트할 수 있는 검증 방법이 필요합니다.

 

CI의 핵심 목표는 버그를 신속하게 찾아 해결하고, 소프트웨어 품질을 개선하고, 새로운 소프트웨어 업데이트를 검증 및 릴리스하는 데 걸리는 시간을 단축하는 것입니다.


CD (Continuous Delivery & Deploy)

CD는 지속적 전달 또는 배포를 의미합니다.

 

CI에서 공유 레포지토리에 수정된 코드를 빌드 및 테스트 후 병합(Merge)를 통해 통합합니다.

이후, 프로덕션 환경에 자동으로 업데이트가 이뤄지는데 이러한 과정을 지속적 전달 또는 지속적 배포라 합니다.

즉, CI에서 확장된 것이 CD라고 할 수 있습니다.


1. Github Actions Workflow 작성

제가 지금부터 제가 구축한 배포 환경에 대해 소개하겠습니다.

 

1. EC2에서 서버, 스토리지, 데이터베이스 등의 컴퓨팅 서비스를 제공 받습니다.

2. CI/CD 플랫폼인 Github Actions를 통해 workflow를 작성합니다.

3. workflow 과정에서 CI/CD가 진행되며 진행되는 과정에서 Docker 서버를 이용해 빌드 이미지를 EC2에 배포합니다.

 

 

우선 workflows를 작성하기 위해선 최상위 폴더에 ".github/workflows/{workflow_name}.yml" 으로 yml 파일을 정의합니다.

 

Events

  • name : workflow 이름을 정의합니다.
  • workflow를 실행하는 특정 활동이나 규칙을 정의합니다.
    • 위의 사진에서는 develop 브랜치로 Push할 시 workflow가 실행됩니다.
    • on : 
          pull_request: 를 하게 되면 Pull Request가 생길 시 실행됩니다.

Jobs

작업(Jobs)은 여러 스텝(Steps)으로 구성되며 종속성 또는 순차적으로 실행된다.

 

  • deploy라는 작업을 생성합니다.
    • runs-on : 가상 머신 운영체제를 정의합니다.
    • name : 각 스텝의 이름을 정의합니다.
    • uses : 어떤 액션을 사용할지 지정합니다. 이미 만들어진 액션을 사용할 때 사용합니다.
    • with : 작업에 대한 설정을 지정합니다.
      • setup-java 
        • distribution : setup-java의 설정으로 JDK 배포 버전을 지정합니다.
        • java-version : 원하는 Java 버전을 지정합니다.
    • run : 실행 명령을 지정합니다.
    • shell : 실행할 쉘을 지정합니다.

 

  • context : Docker 빌드시 사용할 컨텍스트 경로를 지정합니다. 여기서는 현재 디렉토리('.')을 사용하므로 현재 디렉토리의 모든 파일 및 폴더가 Docker 빌드 컨텍스트로 사용됩니다.
  • dockerfile : Docker 빌드 시 사용할 Dockerfile의 경로를 지정합니다. 현재 최상위 폴더에 Dockerfile이 존재하기 때문에 그 Dockerfile을 사용합니다.
  • push : 빌드된 Docker 이미지를 Docker Hub에 푸시할지 여부를 설정합니다.
  • tags : 빌드된 Docker 이미지에 태그를 지정합니다. 현재 latest 태그가 붙은 이미지로 지정되어 있습니다.
  • username 및 password는 Github의 secrets를 이용하여 환경변수를 관리합니다. (이 부분은 쉽게 할 수 있으니 찾아 보시는 걸 추천합니다 !)

 

- name: Deploy
        uses: appleboy/ssh-action@master
        with:
          host: ${{secrets.HOST}}
          username: ubuntu
          key: ${{secrets.PEM_KEY}}
          script: |
            sudo docker pull ${{secrets.DOCKER_USERNAME}}/zerozero:latest

            EXISTING_CONTAINER_ID=$(sudo docker ps -q -f "publish=8080" -f "status=running")
            if [ ! -z "$EXISTING_CONTAINER_ID" ]; then
              sudo docker stop $EXISTING_CONTAINER_ID
              sudo docker rm $EXISTING_CONTAINER_ID
            fi
  
            EXISTING_CONTAINER_ID=$(sudo docker ps -q -f "publish=8080" -f "status=exited")
            if [ ! -z "$EXISTING_CONTAINER_ID" ]; then
              sudo docker rm $EXISTING_CONTAINER_ID
            fi

            sudo docker rm $(sudo docker ps --filter 'status=exited' -a -q)
            sudo docker run -d --log-driver=syslog -p 8080:8080 -e JAVA_OPTS=-Djasypt.encryptor.password=${{secrets.JASYPT_ENCRYPTOR_PASSWORD}} ${{secrets.DOCKER_USERNAME}}/zerozero:latest
            sudo docker image prune -a -f

 

  • appleboy/ssh-action@master : SSH를 통해 원격 서버에 접속하여 명령을 실행합니다. 여기서는 EC2 서버에 접속합니다.
  • host : SSH 연결할 호스트의 IP 주소 또는 도메인을 지정합니다. 여기서는 EC2의 퍼블릭 IP 주소를 지정합니다.
  • username : SSH 접속 시 사용할 사용자 이름을 지정합니다. 여기서는 터미널로 EC2 서버의 username을 사용합니다.
  • key : SSH 접속에 사용할 키파일의 내용을 지정합니다. 여기서는 EC2의 PEM Key를 이용하는데 PEM Key를 메모장으로 연결하여 그 안에 있는 값을 복사해서 secrets에 저장합니다.
  • script : 원격 서버에서 실행할 스크립트를 지정합니다.
    • Docker 이미지를 최신 버전으로 업데이트 하기 위해 Docker Hub에서 이미지를 다운로드합니다.
    • 8080포트를 사용하는 현재 실행중인 Docker 컨테이너를 중지하고 제거합니다. (기존 빌드되던 이미지 제거)
    • 종료된 Docker 컨테이너 또한 제거합니다.
    • 새로운 Docker 컨테이너를 실행하고 8080 포트로 매핑합니다. 저는 Jasypt 암호화를 통해 yml 변수를 암호화했기 때문에JAVA_OPTS를 통해 Jasypt 비밀번호를 이때 전달해서 실행할 때 사용하게끔 합니다.
    • 사용하지 않는 Docker 이미지를 제거합니다.

2. Dockerfile 생성

 

  • FROM : Docker 이미지의 베이스 이미지를 설정합니다. 여기서는 OpenJDK 17 버전의 JCK 이미지를 사용합니다.
  • COPY : 호스트 시스템의 로컬 파일을 Docker 이미지 내부로 복사합니다. 빌드되어 생긴 jar 파일을 Docker 이미지 내부의 /zerozero.jar 로 복사합니다. 즉, Java 어플리케이션의 실행 파일이 위치한 곳을 Docker 이미지 내부로 복사하는 역할을 합니다. 
  • ENTRYPOINT : Docker 컨테이너가 실행될 때 실행할 명령을 지정합니다. 
    • sh -c : Shell을 실행하고 그 안에서 명령을 실행합니다.
    • ${JAVA_OPTS} -jar /zerozero.jar : Shell에서 명령을 실행할 때 ${JAVA_OPTS} 환경 변수와 함께 zerozero.jar를 실행하는 Java 명령을 실행합니다. ${JAVA_OPTS}는 Workflow 파일에서 지정했던 값을 가져옵니다.

정리

1. Checkout Code : Github 레포지토리에 있는 코드를 작업 환경으로 복제합니다.

2. Set up JDK 17 : 현재 프로젝트의 JDK를 작업 환경에 설치합니다.

3. Grant execute permission for gradlew :gradlew 파일에 실행 권한을 부여합니다.

4. Build with Gradle : 프로젝트를 빌드하여 JAR 파일을 생성합니다.

 

5. Build Docker Image : 도커 이미지를 생성합니다.

 

6. Login to Docker : 사용자 계정으로 로그인합니다.

 

7. Push to Docker : 도커 이미지를 Docker Hub에 전달합니다.

 

8. Deploy : EC2와 연결하여 Docker 컨테이너를 생성합니다.

 

설정 후 develop 브랜치에 Push를 하게 되면 위와 같이 workflow가 실행됩니다.

테스트 시 에러가 발생할 경우, Docker로 이미지가 전달되지 않고 EC2로 배포되지 않습니다.

따라서 수정된 코드가 잘못 되었을 경우에도 현재 실행중인 환경에서는 문제가 생기지 않는 장점이 있습니다.


개선 사항

최근에 회사 면접을 봤습니다. 그때 면접관님께서 CI/CD 관련해서 조언을 해주셨습니다. 

조언대로 개선을 하려고 합니다. 다시한번 진심으로 감사합니다 !!

 

1. AWS의 ECS(Elastic Container Service)를 사용합니다. 

AWS EC2를 이용하는데 외부 Docker 서버를 이용하는 것보다 AWS의 ECS를 사용하는 것이 좋을 것 같다고 알려주셨습니다.

 

2. Docker의 이미지 크기를 줄여야 합니다.

Docker의 빌드된 이미지 크기를 줄일 수 있는 방법이 있다고 합니다. 그 방법을 찾아서 이미지 크기를 최적화하려고 합니다.

 

3. Github Actions에서 속도를 단축시키는 방법을 모색합니다.

 

위의 개선 사항을 공부해서 블로그에 수정한 것을 기록하도록 하겠습니다.

감사합니다.