본문 바로가기
CICD

Gighub Actions,docker 배포

by 순원이 2024. 1. 11.

GitHub Actions

GitHub Actions는 일반적으로 CI(Continuous Integration, 지속 통합) 또는 CD(Continuous Deployment, 지속 배포)와 같은 자동화를 위해서 사용됩니다. 운영환경에 소스코드를 머지하기 위해 반복적으로 처리되야 할 일들들을 처리해줍니다. 즉, CI/CD를 위한 파이프라인입니다. 

CD ( Code Deploy)

Source Code를 운영환경에 자동 배포하는 역할을 수행하는 행위(지속적 자동 배포)

 

전체적인 플로우

1. Github Action에서 소스코드를 빌드한다.

2. Github Action에서 빌드한 소스코드를 이미지로 빌드한다.

3. Github Action에서 빌드한 이미지를 Docker 레퍼지토리에 넣는다.

4. Github Action에서 Ec2에 접속하여 Docker 레퍼지토리에 있는 이미지를 pull한다.

 

 

 

Action 탭에 들어가 템플릿을 선택해 cicd.yml 파일을 만들어줘도 되고 workflows 디렉토리에 yml 파일을 직접 만들어도 됩니다. 저는 직접 만들었습니다.

 

 

전체코드

# 실행 시기
on:
  push:
    branches: develop
  pull_request:
    branches: develop
    
# 작업 내용
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Chekcout Main Branch
        uses: actions/checkout@v3

      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: 17
          distribution: 'temurin'
          
      - name: Set application.yml  
        uses: microsoft/variable-substitution@v1
        with:
          files: src/main/resources/application.yml
        env:
          jwt.secretkey: ${{ secrets.SECRETKEY }} 
          cloud.aws.credentials.access-key: ${{ secrets.AWS_ACCESSKEY }} 
          cloud.aws.credentials.secret-key: ${{ secrets.AWS_SECRETKEY }} 
          imp.secret-key: ${{ secrets.API_SECRET_KEY }}
          imp.key: ${{ secrets.API_KEY }}
        
    
      ## 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-

      - name: Grant Permission Gradlew
        run: chmod +x gradlew
        shell: bash

      - name: Build Gradle
        run: ./gradlew build
        shell: bash

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

         ## deploy to develop
      - name: Deploy to dev
        uses: appleboy/ssh-action@master
        id: deploy-dev
        if: contains(github.ref, 'develop')
        with:
          host: ${{ secrets.HOST_DEV }}
          username: ${{ secrets.USERNAME }}
          password: ${{ secrets.PASSWORD }}
          key: ${{ secrets.PRIVATE_KEY }}
          port: 22
          script: |
            sudo docker ps
            docker pull ${{ secrets.DOCKER_USERNAME }}/backend:latest
            sudo docker run -d -i -p 8081:8081 ${{ secrets.DOCKER_USERNAME }}/backend
            sudo docker image prune -f

코드가 길기 때문에 나눠서 설명드리겠습니다.

실행시기 설정

# 실행 시기
on:
  push:
    branches: develop
  pull_request:
    branches: develop
  • develop에 push가 되거나 pull request가 발생하면 위 워크플로우가 실행됩니다.

작업환경 설정

# 작업 내용
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Chekcout Main Branch
        uses: actions/checkout@v3

      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: 17
          distribution: 'temurin'

  • 최신 버전의 Ubuntu 운영체제에서 각 step 별로 실행되고
  • actions/checkout@v3: CI 서버로 코드를 내려받기
  • actions/setup-java@v3: 자바17을 설치

application.yml파일 내에 환경변수 설정

	    - name: Set application.yml  
        uses: microsoft/variable-substitution@v1
        with:
          files: src/main/resources/application.yml
        env:
          SECRETKEY: ${{ SECRETKEY }} 
          AWS_ACCESSKEY: ${{ AWS_ACCESSKEY }} 
          AWS_SECRETKEY: ${{ AWS_SECRETKEY }} 
          API_SECRET_KEY: ${{ API_SECRET_KEY }}
          API_KEY: ${{ API_KEY }}
        
      - name: Set loback-spring.xml  
        uses: microsoft/variable-substitution@v1
        with:
          files: src/main/resources/loback-spring.xml  
        env:
          SLACK_WEBHOOK_URI: ${{ SLACK_WEBHOOK_URI } }

yml 파일에 시크릿 값이 있습니다 예를 들어 JWT 토큰 시크릿 값처럼요. 개발환경에서는 Intellij 환경변수 설정에서 주입해줄 수 있지만 운영환경에서는 따로 주입 해줘야겠죠? 이코드들이 yml 시크릿값 환경변수 설정해주는 코드입니다.


🗨️ 잠시 환경변수를 등록하는 방법을 알아보겠습니다.

Secret 환경 변수 등록하기

Setting → Secrets and variables → Actions

New repository secret 클릭

 

cicd.yml에서 사용한 ${DOCKER_USERNAME} 와 같은 시크릿 값들을 설정해주면 됩니다

결과

코드에서 사용되는 시크리값들 즉, application-dev.yml 파일들에서 시크릿값으로 처리되어있는 것들을 어떻게 해야될까요?

  1. application.yml파일을 .gitignore에 추가하고(깃허브에 올리지 않는다.) 깃허브액션에서 새로 application-dev.yml를 만들어 주입시켜준다
## create application-dev.properties
      - name: make application-dev.properties
        if: contains(github.ref, 'develop')
        run: |
          cd ./src/main/resources
          touch ./application-dev.properties
          echo "${{ secrets.PROPERTIES_DEV }}" > ./application-dev.properties
        shell: bash

2. 깃허브액션에서 application-dev.yml에 있는 환경변수 값들을 직접 주입해준다.

- name: Set yml file 
      uses: microsoft/variable-substitution@v1
      with:
        files: ${{ env.RESOURCE_PATH }} 
      env:
        spring.datasource.url: ${{ secrets.RDS_HOST }} 
        spring.datasource.username: ${{ secrets.RDS_USERNAME }} 
        spring.datasource.password: ${{ secrets.RDS_PASSWORD }} 
        jwt.key: ${{ secrets.JWT_KEY }}
        naver.map.client.id: ${{ secrets.NAVER_CLIENT_ID }}
        naver.map.client.secret: ${{ secrets.NAVER_CLIENT_SECRET }}

저는 application.yml 파일을 깃허브에 올림으로서 팀원들과 변경에 관해 바로 공유하고 싶기 때문에2번 방식을 사용하도록 하겠습니다.


마저 cicd.yml 파일 설명해 보겠습니다

gradle 설정

	
			 ## 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-

			- name: Grant Permission Gradlew
        run: chmod +x gradlew
        shell: bash

			- name: Build Gradle
        run: ./gradlew build
        shell: bash

  • gradle 빌드의 속도를 향상시키기 위해 gradle의 캐시를 깃액션의 캐싱 시스템의 저장합니다.
  • chmod +x gradlew: gradle에게 실행권한 부여
  • ./gradlew build: 프로젝트를 테스트하기 위해 build 합니다

도커 build 및 push 생성

     ## docker build & push to develop
      - name: Docker build & push to dev
        if: contains(github.ref, 'develop')
        run: |
            docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
            docker build -t ${{ secrets.DOCKER_USERNAME }}/backend .
            docker push ${{ secrets.DOCKER_USERNAME }}/backend
  • DOCKER_USERNAME: 도커 ID
  • DOCKER_PASSWORD: 도커 계정 패스워드
  • DOCKER_REPO: 도커 리포지토리
  • 도커에 로그인 하고 uglypotato-dev라는 태그를 만든 후 도커 리포지터리에 push
  • Docker가 Dockerfile을 읽어서 자동으로 도커 이미지를 빌드합니다**.** 만약, prod와 dev를 나눠서 빌드하실 때는 “-f {파일명}” 명령어를 추가하시면 됩니다.

Ec2에서 도커 pull 받기

   ## deploy to develop
      - name: Deploy to dev
        uses: appleboy/ssh-action@master
        id: deploy-dev
        if: contains(github.ref, 'develop')
        with:
          host: ${{ secrets.HOST_DEV }}
          username: ${{ secrets.USERNAME }}
          password: ${{ secrets.PASSWORD }}
          key: ${{ secrets.PRIVATE_KEY }}
          port: 22
          script: |
            sudo docker ps
            docker pull ${{ secrets.DOCKER_USERNAME }}/backend:latest
            sudo docker run -d -i -p 8081:8081 ${{ secrets.DOCKER_USERNAME }}/backend
            sudo docker image prune -f
  • HOST_DEV: dev 환경의 EC2 인스턴스 ip
  • USERNAME: EC2 인스턴스 계정 ID(ec2-user)
  • PRIVATE_KEY: EC2 인스턴스 SSH키
  • 위에서 이미지를 생성 후 도커 리포지터리에 push 해줬습니다. 그것을 Ec2에서 받아오는 것입니다.

Dockerfile

# 기본 이미지 설정
FROM openjdk:17-jdk-slim

# GitHub Actions에서 빌드한 JAR 파일 복사
COPY build/libs/potato-API-server-0.0.1-SNAPSHOT.jar potato-dev.jar

# 컨테이너 실행 명령 설정
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/potato-dev.jar"]

build/libs/ 에 들어가 보시면 plain.jar 파일 있을 것입니다.

plain.jar: 어플리케이션 실행에 필요한 모든 의존성을 포함하지 않고, 작성된 소스코드의 클래스 파일과 리소스 파일만 포함한다.

사용하지 않으실 거면 build.gradle 파일에 아래와 같이 설정을 해주면 plain.jar 파일을 생성하지 않을 수 있습니다.

plugins {
 ...
}

...

jar {
    enabled = false

}

...

AWS EC2 인스턴스 도커,도커 컴포즈 설치

// 도커 설치
sudo yum install docker -y

// 도커 실행
sudo service docker start

//Docker 그룹에 sudo 추가 (인스턴스 접속 후 도커 바로 제어할 수 있도록)
sudo usermod -aG docker ec2-user

//인스턴스 재접속 후 Docker 명령어 실행해보기 
docker run hello-world

// Docker 관련 권한 추가
sudo chmod 666 /var/run/docker.sock
docker ps

// 도커 컴포즈(도커 컨테이너를 다루는 도구) 설치
sudo curl \\
-L "<https://github.com/docker/compose/releases/download/1.26.2/docker-compose-$>(uname -s)-$(uname -m)" \\
-o /usr/local/bin/docker-compose

// 권한 추가
sudo chmod +x /usr/local/bin/docker-compose

sudo dnf install libxcrypt-compat

// 버전 확인
docker-compose --version

도커 허브 계정 만들기 → 리포지토리 생성 → putty에서 도커 접속 → 도커 tag 생성

자세한 과정은 아래 링크를 참고해보세요 ~

도커 리포지터리 만들기 링크


문제점

첫 번째 시도는 정상적으로 동작했지만 두 번째 시도했을 때 문제가 생겼습니다. 

ec2에서 도커 이미지를 받고나서 문제가 있는 것 같습니다

새 컨테이너가 이미 사용 중인 포트(8081)에 바인딩하려고 시도하여 에러가 발생했습니다.

가장 간단한 해결법으로는 실행중인 컨테이너를 확인하고 stop 하고 새로운 컨테이너를 올리는 명령어를 cicd.yml에 추가하는 방식이 있을 것 같습니다.

그러나, 이과정에서 컨테이너가 stop될 때 동안 사용자들은 오류를 겪게 되겠죠? 개발자는 사용자의 경험을 중요시하여야 합니다. 즉, 무중단 배포가 중요하기에 무중단 배포에 대해서 알아봤습니다.

 

무중단 배포를 위한 설명은 아래 링크를 클릭해주세요!