Experience/항해99 18기

[챌린지 프로젝트 사전주차] CI-CD : Github Actions, Docker, AWS EC2로 자동배포를 해보자

chillmyh 2024. 1. 26. 13:06

1. Docker가 뭔데?

도커(Docker)는 컨테이너 기반 가상화 플랫폼으로, 응용 프로그램과 그 종속성을 격리된 환경인 컨테이너로 패키징하여 실행하는 기술이다. 이를 통해 응용 프로그램을 서로 다른 환경에서도 일관되게 실행할 수 있고, 개발 환경과 운영 환경 사이의 차이로 인한 문제를 줄일 수 있다.

본 프로젝트에서는 AWS S3와 CodeDeploy 조합 대신 Docker를 선택했는데 이유는 아래와 같다.
Docker 컨테이너는 이식성이 뛰어나고 Docker 지원을 통해 모든 시스템에서 일관되게 실행될 수 있어 S3 및 CodeDeploy를 사용한 기존 배포에 비해 플랫폼 독립성이 뛰어나다. 또한, CodeDeploy는 AWS 환경과의 연동에 대한 이해와 학습곡선이 존재하고, 환경별로 관리해줘야한다는 단점이 있다하여 이번 2주동안 진행하는 프로젝트에서는 Docker를 사용하였다.

Docker를 ubuntu(EC2)로 사용하였는데 자주 사용되는 명령어들이 있다.

1-2) Docker 자주쓰는 명령어

  • 실행중인 컨테이너 목록 확인
    docker ps
  • 전체 컨테이너 목록 확인 (실행중이지 않는 것까지 전부)
    docker ps -a
  • 컨테이너 생성 및 시작
    docker run 컨테이너ID
  • 컨테이너 중지
    docker stop 컨테이너ID
  • 중지된 컨테이너 ID를 가져와서 한번에 삭제
    docker rm -v $(docker ps -a -q -f status=exited)
  • 컨테이너 제거
    docker rm 컨테이너ID
  • 이미지 목록 확인
    docker images
  • 이미지 pull
    docker image pull 레파지토리명[:태그명]
  • 이미지 삭제
    docker rmi 이미지ID
  • 실시간 컨테이너의 로그 확인
    docker logs -f 컨테이너ID
  • 컨테이너 빠져나오기
    exit

2. 직접 적용해보자!

우리는 Docker image를 빌드하여 Docker Hub로 image를 push해주고, 원격으로 EC2 ubuntu에 접속하여 push했던 image를 pull 받아 run 시켜서 서버에 배포할것이다!

다음 순서대로 진행하면 된다.

2-1) Docker Hub Repository 생성

  • Docker Hub 에 접속하여 회원가입.
    이때 쓰인 username과 password가 나중에 github-actions.yml 에 secrets 값으로 들어간다.
  • Repositories -> Create repository -> 원하는 레퍼지토리 명을 입력하고 Public으로 생성

2-2) Docker 설치

  • 혹시 설치되어있을 도커 구버전 삭제
    sudo apt-get remove docker docker-engine docker.io containerd runc
  • 도커 설치 전 기타 세팅들
    sudo apt-get update
sudo apt-get install \
    ca-certificates \
    curl \
    gnupg \
    lsb-release
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | 
sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
echo \
  "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
  $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
  sudo apt-get update
  • 도커 설치
    sudo apt-get install docker-ce docker-ce-cli containerd.io
  • 설치 후, 잘 설치되어있는지 --version으로 확인해보자
    docker --version

2-3) github-actions.yml 세팅

본 프로젝트에서는 'be/dev' branch에 push 될때만 배포되도록 세팅해뒀다.

아래는 Github Actions -> Docker Hub -> EC2 흐름의 자동배포 세팅이다.

# github repository actions 페이지에 나타날 이름  
name: CI/CD using github actions & docker  

# event trigger  
# "be/dev" 브랜치에 push가 되었을 때 실행  
on:  
  push:  
    branches: [ "be/dev" ]  

permissions:  
  contents: read  

jobs:  
  CI-CD:  
    runs-on: ubuntu-latest  
    steps:  

      # JDK setting - github actions에서 사용할 JDK 설정
      - uses: actions/checkout@v3  
      - name: Set up JDK 17  
        uses: actions/setup-java@v3  
        with:  
          java-version: '17'  
          distribution: 'temurin'  

      # gradle caching - 빌드 시간 향상  
      - name: Gradle Caching  
        uses: actions/cache@v3  
        with:  
          path: |  
            ~/.gradle/caches  
            ~/.gradle/wrapper  
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}  
          restore-keys: |  
            ${{ runner.os }}-gradle-  

      # 환경별 yml 파일 생성
      - name: make application.yml  
        if: contains(github.ref, 'be/dev')  
        run: |  
          cd ./src/main/resources  
          touch ./application.yml  
          echo "${{ secrets.YML_DEV }}" > ./application.yml  
        shell: bash

      # gradle build      
      - name: Build with Gradle  
        run: ./gradlew build -x test  


      # docker build & push to develop      
      - name: Docker build & push to dev  
        if: contains(github.ref, 'be/dev')  
        run: |  
          docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}  
          docker build -t ${{ secrets.DOCKER_USERNAME }}/deliveryrepository_dev .  
          docker push ${{ secrets.DOCKER_USERNAME }}/deliveryrepository_dev  

      ## deploy to develop  
      - name: Deploy to dev  
        uses: appleboy/ssh-action@master  
        id: deploy-dev  
        if: contains(github.ref, 'be/dev')  
        with:  
          host: ${{ secrets.HOST_DEV }} # EC2 퍼블릭 IPv4 DNS#          username: ${{ secrets.USERNAME }} # ubuntu  
          port: 22  
          key: ${{ secrets.PRIVATE_KEY }}  
          envs: GITHUB_SHA  
          script: |  
            sudo docker ps  
            docker stop $(docker ps -a -q)  
            docker rm $(docker ps -a -q)  
            sudo docker pull ${{ secrets.DOCKER_USERNAME }}/deliveryrepository_dev  
            sudo docker run -d -p 8080:8080 ${{ secrets.DOCKER_USERNAME }}/deliveryrepository_dev  
            sudo docker image prune -f

이제, docker가 사용되는 부분들을 살펴보자.

  • docker build & push to dev
# docker build & push to dev    
      - name: Docker build & push to dev  
        if: contains(github.ref, 'be/dev')  
        run: |  
          docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}  
          docker build -t ${{ secrets.DOCKER_USERNAME }}/deliveryrepository_dev .  
          docker push ${{ secrets.DOCKER_USERNAME }}/deliveryrepository_dev  

docker hub에 로그인 후, docker image를 build -> docker hub에 image를 push한다.
USERNAME, PASSWORD는 Docker hub의 id와 password이다.

  • deploy to dev
## deploy to dev
      - name: Deploy to dev  
        uses: appleboy/ssh-action@master  
        id: deploy-dev  
        if: contains(github.ref, 'be/dev')  
        with:  
          host: ${{ secrets.HOST_DEV }} # EC2 퍼블릭 IPv4 DNS#          username: ${{ secrets.USERNAME }} # ubuntu  
          port: 22  
          key: ${{ secrets.PRIVATE_KEY }}  
          envs: GITHUB_SHA  
          script: |  
            sudo docker ps  
            docker stop $(docker ps -a -q)  
            docker rm $(docker ps -a -q)  
            sudo docker pull ${{ secrets.DOCKER_USERNAME }}/deliveryrepository_dev  
            sudo docker run -d -p 8080:8080 ${{ secrets.DOCKER_USERNAME }}/deliveryrepository_dev  
            sudo docker image prune -f

중요한 부분은 script 부분이다.

  • sudo docker ps : actions 진행 중 현재 실행중인 docker 컨테이너를 보여준다. 확인용이라 없어도 되는 부분.
  • docker stop ~ : 실행중인 docker 컨테이너를 중지한다.
  • docker rm ~ : 중지된 컨테이너를 삭제한다.
  • sudo docker pull ~ : docker hub에서 image를 pull 받아온다.
    • sudo docker run ~ : 8080:8080 포트로 도커 이미지를 실행한다.
  • sudo docker image prune -f : 불필요한 이미지 리소스들을 삭제한다.

이렇게 세팅이 끝나면 다음과 같은 과정으로 자동배포가 완성된다!

'be/dev' branch에 push가 일어날때
-> gradle build
-> docker image build
-> image push to docker hub
-> image pull from docker hub
-> image(container) run


🎯 트러블슈팅

1) Docker Run을 통하여 배포 직후, 서버 상태가 바로 Exited 로 변경되어 배포 자동 중단 현상

  • 원인 추론 :
    프로젝트 application.properties or yaml에 명시되어있는 DB 설정 정보와 배포할때 사용할 DB (EC2, RDS)의 정보가 달라 서버 실행 중 자동 종료 됐던 것.
  • 해결 방법 :
    RDS 데이터베이스 생성 (RDS 인바운드 규칙 편집 추가 0.0.0.0/0)

1-2) RDS를 IntelliJ Data Source에 연결하려하니 Test Connecting 무한로딩 현상 발생

  • 해결 방법 :
    RDS 데이터베이스 생성할 때 설정 중에서 아래와 같이 변경하여 해결

1-3) Gradle 빌드가 되지 않는 현상 발생

  • 해결 방법 :
    application.properties에 DataSource로 MySQL RDS 정보를 제대로 다시 입력한 후 빌드해서 해결

spring.datasource.url=연결한 database url spring.datasource.username=RDS 유저네임 spring.datasource.password=RDS 비밀번호 spring.jpa.hibernate.ddl-auto=update

2) GitHub Actions를 이용한 CI / CD 중 Gradle을 통한 Build가 되지 않는 현상

./gradlew build permission denied

  • 환경 : intel MAC
  • 원인추론 :
    맥 터미널에 설정되어있는 자바의 버전과 프로젝트의 자바 버전이 맞지 않아 ./gradlew build 명령어가 실패한 것
  • 해결 방법 :
    java —version 으로 확인해보니 터미널상에서는 Open jdk 21이고 IntelliJ와 gradle 자바 버전은 17이었다. 터미널에서 자바설정을 프로젝트 환경에 맞게 변경해주면 된다.원하는 jdk로 변경 :
    export JAVA_HOME=$(/usr/libexec/java_home -v 11.0.17) source ~/.bash_profile
  • 설치된 모든 jdk 버전들 확인 :
    /usr/libexec/java_home -V

2-1) 터미널에서 자바 버전을 변경하였지만 적용되지않고 계속 버전이 초기화되는 현상

  • 해결 방법 :
    default jdk 를 설정해주지 않아서 생긴 문제였다. 따로 자바 환경변수 설정을 안해주면 터미널을 새로 열때마다 설정한 java version이 초기화된다. zsh를 사용하고 있었으므로 ~/.zshrc 에 자바 버전을 설정해줘야한다. 반드시 설정 후, source ~/.zshrc 명령어를 통하여 설정을 완료해줘야한다.

3) 자동 배포 단계에서 PRIVATE-KEY를 찾을 수 없는 현상

  • 해결방법 :
    ec2의 키페어 파일을 vim (키파일) 로 열어봤었는데 vim으로 확인되는 키값에 누락이 있었다.
    이 문제는 키페어 파일을 Visual Studio Code 로 열면 온전하게 누락없이 키값을 확인할 수 있었다!!!!
    (나중에 알게된 사실로는 vim으로 접속했을때 키보드 방향키로 내리면 누락없이 값을 전부 확인가능하다)
    이 코드를 ----BEGIN RSA PRIVATE KEY---— 부터 ----END RSA PRIVATE KEY---— 까지 주석 전부 포함해서 GitHub Secrets에 등록해서 해결했다.

4) 깃허브 액션 deploy to develop 부분에서 docker/io timeout 오류

  • 원인추론 :
    EC2 SSH 소스유형이 내IP로 되어있어 GitHub Actions 의 IP가 접근 불가한 상태였음
  • 해결 방법 :
    EC2 보안그룹에 인바인드 규칙으로 내 IP만 허용이 아닌 모든 IP(0.0.0.0/0) 허용으로 변경

5) 터미널에서 ssh ec2 접속시에 Permission denied (publickey) 에러 현상

  • 원인추론:
    기존에 등록되어있던 키페어의 내부 키값에 손상이 있어 기존 EC2에 새로운 키페어를 연결하려다가 SSH에 키파일이 두개 등록되어있었다. 기존 키를 삭제해도 전에 키파일 권한 기록이 남아있어서 그랬는지 해당 에러로 진행이 되지 않았음
  • 해결 방법 :
    기존 ec2 인스턴스와 키페어들을 전부 삭제하고 새로 인스턴스, 키페어를 생성하고 진행하여 해결했다.

5-1) WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@

  • 원인추론 :
    원격 호스트 식별 정보가 변경되었다.기존에 등록되어있던 RSA key가 다르다.
  • 해결 방법 :
    root에는 본인 터미널 host 명
    본인 같은 경우에는 root에 hyun을 입력하였음.
[root@host~]# rm /root/.ssh/known_hosts

6) zsh: ./gradlew: bad interpreter: /bin/sh^m: no such file or directory 스프링 부트 빌드 에러

  • 환경 :
    intel Mac 원인 : 팀원의 운영체제가 윈도우였고, 본인은 맥이었기 때문에 gradlew 스크립트에 ^M 부분 차이가 생겨 빌드를 할 수 없었던 상태
  • 해결 방법 :
    dos2unix를 사용해 해결
    dos2unix 설치 : brew install dos2unix
    터미널에 입력 : dos2unix ./gradlew

3. 결과

Github Actions와 Docker를 이용해 EC2 서버에 자동배포를 성공했다!
하지만 아직 아쉬운점이 있는데, 새로운 내용을 push하여 서버가 업데이트 될 때 서버가 잠깐 닫히는 downtime이 존재하여 연속적인 서비스 운영을 할 수 없다. 때문에, 무중단배포가 필요한데.. 그 내용은 다음 글에서 프로젝트에 적용했던 무중단배포에 대해서 적어보겠다.