[完] Github Action을 활용한 배포 자동화 CI/CD - (8) Github Actions CI/CD + docker-compose

유아 Yooa·2023년 1월 20일
8

CI/CD

목록 보기
8/9

Overview

지난 포스팅까지 도커에 nginx를 올리고, reverse proxy 설정까지 마쳤다. 이제 Github Actions를 이용하여 스프링부트를 자동 배포해보자.


1. Dockerfile 작성하기

스프링 프로젝트를 Docker 컨테이너에 올리기 위해서는Dokerfile을 작성해야 한다. Spring Boot로 만들어진 Web Application을 Docker Image로 만들어서 docker hub에 push하는 과정이다.

스프링부트 프로젝트 루트 경로에 Dockerfile 이라는 파일을 생성해서 아래와 같은 내용을 입력하자.

FROM openjdk:11
ARG JAR_FILE=./build/libs/{빌드한 jar 파일명}.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar", "--spring.profiles.active=prod"]

FROM openjdk:11
: base 이미지를 openjdk:11로 설정한다. 즉, Docker에 올릴 때 jdk11 버전을 이용해서 올리겠다고 선언해주는 커맨드이다.

ARG JAR_FILE=./build/libs/{빌드한 jar 파일명}.jar
: JAR 파일의 위치를 환경변수의 형태로 선언해주는 커맨드이다. 프로젝트를 빌드하게 되면 build/libs/{프로젝트명}-0.0.1-SNAPSHOT.jar 형태로 파일이 생성된다.

COPY ${JAR_FILE} app.jar
: 프로젝트 빌드 파일을 컨테이너의 루트 디렉토리의 app.jar 라는 이름으로 복사하는 커맨드이다.

ENTRYPOINT ["java","-jar","/app.jar", "--spring.profiles.active=prod"]
: 컨테이너에서 java -jar /app.jar를 실행하는 커맨드이다. 이때 --spring.profiles.active=prod 라는 옵션은 properties 파일에서 profiles를 활성화하는 경우 붙여준다.


2. docker-compose 설정하기

2-1. docker-compose 설치하기

지난번 EC2 인스턴스에서 docker-compose를 설치했으니 스킵하도록 하겠다.

2-2. docker-compose.yml 작성하기

docker-compose 파일을 작성하여 EC2 서버에 배포할 것이다. 프로젝트 루트 경로에 docker-compose.yml 파일을 생성하자.

docker-compose.yml을 작성하여 각각 독립된 컨테이너의 실행 정의를 실시한다.

version: "3" # 버전 지정

services: # 컨테이너 설정
  database: 
    container_name: mysql # 컨테이너 이름
    image: mysql/mysql-server:latest # 컨테이너에서 사용하는 base image 지정
    environment: # 컨테이너 안의 환경변수 설정
      MYSQL_DATABASE: {database name}
      MYSQL_USER: {database user}
      MYSQL_PASSWORD: {database pwd}
      MYSQL_ROOT_HOST: '%'
      MYSQL_ROOT_PASSWORD: rootpwd
    command: # 명령어 설정
      - --default-authentication-plugin=mysql_native_password
    ports: # 접근 포트 설정 
      - 3305:3306 # Host:Container
    networks:
      - db_network
    restart: always  # 컨테이너 실행 시 재시작
  mytamla:
    build: .
    expose:
      - 8080
    depends_on:
      - database

networks: # 커스텀 네트워크 추가
  db_network: # 네트워크 이름
    driver: bridge

방금 작성한 Dockerfile로 스프링부트 컨테이너를 실행하고, mysql은 기존 이미지를 사용해 컨테이너 실행을 할 것이다.

mysql config를 따로 빼서 볼륨 설정을 해주기도 하는데 필자는 생략했다.

기본적으로 Docker Compose는 하나의 디폴트 네트워크에 모든 컨테이너를 연결한다. 디폴트 네트워크의 이름은 docker-compose.yml가 위치한 디렉토리 이름 뒤에 _default가 붙는다.

커스텀 네트워크
컨테이너에 올리는 mysql은 호스트 컴퓨터에서 접속할 때는 3305포트를 사용해야 하고, 같은 디폴트 네트워크 내의 다른 컨테이너에서 접속할 때는 3306 포트를 사용해야 한다.

이렇게 해주면 database 서비스는 디폴트 네트워크 뿐만 아니라 db_network 네트워크에도 연결되게 된다.

여러가지 네트워크 드라이버가 있는데

  1. bridge : 하나의 호스트 컴퓨터에서 여러 개의 컨테이너들이 통신할 수 있게 한다.
  2. host : 호스트 컴퓨터와 동일한 네트워크에서 여러 개의 컨테이너들이 통신할 수 있게 한다.
  3. overlay : 여러 호스트 컴퓨터(다른 네트워크)에서 여러 개의 컨테이너들이 통신할 수 있게 한다.

3. Github Action CI/CD 작성하기

3-1. CI/CD

CI(Continuous Integration)는 지속적 통합을 나타내는 용어이다.

어플리케이션의 새로운 코드 변경 사항이 정기적으로 빌드 및 테스트되어 공유 레포지토리에 통합되는 것을 의미한다.

클래스와 기능에서부터 전체 애플리케이션을 구성하는 서로 다른 모듈에 이르기까지 모든 것에 대한 테스트를 수행할 수 있으며, 코드를 병합하는 과정에서 충돌이 생긴다면 CI를 통해 버그를 수정할 수 있다.

이로 인해 개발하는 코드의 품질을 좀 더 향상시킬 수 있으며, 새로운 업데이트의 검증 및 릴리즈의 시간을 단축시킬 수 있다.

CD(Continuous Deliver, Countinuous Deplotment)는 지속적인 배포를 나타내는 용어이다.
(* 보통은 전자인 지속적 제공의 의미가 강하다.)

CI/CD는 지속적인 통합과 지속적인 배포의 결합된 관행을 뜻하며 이 과정들을 자동화하여 불필요한 공수를 줄이고 보다 빠른 서비스 제공을 할 수 있는 효과를 가진다.

3-2. 왜 CI/CD를 구축하는걸까?

어플리케이션 개발 단계를 자동화하면 보다 짧은 주기로 고객에게 제공할 수 있다. 자동화가 이루어지지 않는다면 모든 빌드와 배포 작업을 수동으로 조작해야 하는데 사소한 수정 사항 하나에도 모든 과정을 거쳐야하므로 매우 불편하고 시간 소요가 많다.

CI/CD 구축을 지원하는 툴은 시중에 많다.

  • Jenkins
  • Travis CI
  • Circle CI
  • Google Cloud Build
  • AWS CodeBuild
  • Github Actions

각 툴은 요금체계부터 특성이 모두 다르므로 프로젝트에 적합한 것을 선택하면 된다.

✅ Github Actions

Github Actions는 소프트웨어 개발 라이프사이클 안에서 PR, push 등의 이벤트 발생에 따라 자동화된 작업을 진행할 수 있게 해주는 기능이다.
CI/CD나 Testing, Cron Job 등 작업을 수행할 수 있다.

✅ Github Actions의 구성 요소

Github Actions를 활용하기 위해 구성 요소를 먼저 파악해보자.

Workflow

  • Workflow란 레포지토리에 추가할 수 있는 일련의 자동화된 커맨드 집합이다.
  • 하나 이상의 Job으로 구성되어 있고, PR이나 push과 같은 이벤트에 의해 실행될 수도 있고 특정 시간대에 실행될 수 있다.
  • 빌드, 테스트, 배포 등 각각의 역할에 맞는 Workflow를 추가할 수 있고, .github/workflows 디렉토리에 YAML 형태를 저장합니다.

Event

  • Event란 Workflow를 실행시키는 push, PR, commit 등의 특정 행동을 의미한다.

Job

  • Job이란 동일한 Runner에서 실행되는 여러 Step의 집합을 의미한다.
  • 기본적으로 하나의 Workflow 내의 여러 Job은 독립적으로 실행되지만, 필요에 따라 의존 관계를 설정하여 순서를 지정해줄 수 있다.

Step

  • Step이란 커맨드를 실행할 수 있는 각각의 Task를 의미한다. Shell 커맨드가 될 수도 있고, 하나의 Action이 될 수도 있다.
  • 하나의 Job 내에서 각각의 Step은 다양한 Task로 인해 생성된 데이터를 공유할 수 있다.

Action

  • Action이란 Job을 만들기 위해 Step을 결합한 독립적인 커맨드로, 재사용이 가능한 Workflow의 가장 작은 단위의 블럭이다.
  • 직접 만든 Action을 사용하거나 Gihub Community에 의해 생성된 Action을 불러와 사용할 수 있다.

Runner

  • Runner란 Github Actions Workflow 내에 있는 Job을 실행시키기 위한 어플리케이션이다.
  • Runner Application은 Github에서 호스팅하는 가상 환경 또는 직접 호스팅하는 가상 환경에서 실행 가능하며, Github에서 호스팅하는 가상 인스턴스의 경우에는 메모리 및 용량 제한이 존재한다.

3-3. CI Workflow 생성하기

필자는 팀원들과 Git을 공유하며 사용하는 환경이므로, 기능 추가와 같이 코드가 수정되고 난 후 feat -> main PR에서 CI가 동작하도록 구성하려고 한다.
(* 혼자 백엔드를 맡아서 feat, main으로만 브랜치를 구성했었다.)

CI 작업을 수행할 프로젝트의 레포지토리의 Actions 탭을 접속한다.

프로젝트 루트 .github/workflows 디렉토리 내에 .yml 파일을생성해도 된다.

다양한 용도별 템플릿을 제공해주고 있다. 지금은 가장 기본적인 템플릿을 사용해볼 것이다. 상단에 set up a workflow yourself 를 클릭한다.

# Workflow 이름은 구별이 가능할 정도로 자유롭게 적어주어도 된다. 
# 필수 옵션은 아니다.
name: Java CI with Gradle

# main 브랜치에 PR 이벤트가 발생하면 Workflow가 실행된다.
# 브랜치 구분이 없으면 on: [pull_request]로 해주어도 된다.
on:
  pull_request:
    branches: [ "main" ]

# 테스트 결과 작성을 위해 쓰기권한 추가
permissions: write-all


# 해당 Workflow의 Job 목록
jobs:
	# Job 이름으로, build 라는 이름으로 Job이 표시된다.
  build:
  	# Runner가 실행되는 환경을 정의
    runs-on: ubuntu-latest

	# build Job 내의 step 목록
    steps:
      # uses 키워드를 통해 Action을 불러올 수 있다.
      # 해당 레포지토리로 check-out하여 레포지토리에 접근할 수 있는 Acion 불러오기
    - uses: actions/checkout@v3
    # 여기서 실행되는 커맨드에 대한 설명으로, Workflow에 표시된다. 
    # jdk 세팅
    - name: Set up JDK 11
      uses: actions/setup-java@v3
      with:
        java-version: '11'
        distribution: 'temurin'
        
      # gradle 캐싱
    - 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-
      
    ### CI
    #gradlew 권한 추가
    - name: Grant Execute Permission For Gradlew
      run: chmod +x gradlew
    
    #test를 제외한 프로젝트 빌드
    - name: Build With Gradle
      run: ./gradlew build -x test

    #test를 위한 mysql설정
    - name: Start MySQL
      uses: samin/mysql-action@v1.3
      with:
        host port: 3305
        container port: 3305
        mysql database: '{database name}'
        mysql user: '{database user}'
        mysql password: '{database pwd}'

    #테스트를 위한 test properties 설정
    - name: Make application-test.properties
      run: |
        cd ./src/test/resources
        touch ./application.properties
        echo "${{ secrets.PROPERTIES_TEST }}" > ./application.properties
      shell: bash

    #test코드 빌드
    - name: Build With Test
      run: ./gradlew test

    #테스트 결과 파일 생성
    - name: Publish Unit Test Results
      uses: EnricoMi/publish-unit-test-result-action@v1
      if: ${{ always() }}
      with:
        files: build/test-results/**/*.xml
        

3-4. CD workflow 작성하기

CI/CD를 분리해 작성했다.

name: CD

on:
  push: #해당 브랜치에 push(merge) 했을 때
    branches:
      - main

permissions: write-all #테스트 결과 작성을 위해 쓰기권한 추가

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      #jdk 세팅
      - uses: actions/checkout@v3
      - name: Set up JDK 11
        uses: actions/setup-java@v3
        with:
          java-version: '11'
          distribution: 'temurin'

      #gradle 캐싱
      - 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-
      ### CD
      #배포를 위한 prod properties 설정
      - name: Make application-prod.properties
        run: |
          cd ./src/main/resources
          touch ./application-prod.properties
          echo "${{ secrets.PROPERTIES_PROD }}" > ./application-prod.properties
        shell: bash

      #test를 제외한 프로젝트 빌드
      - name: Build With Gradle
        run: ./gradlew build -x test

      #도커 빌드 & 이미지 push
      - name: Docker build & Push
        run: |
          docker login -u ${{ secrets.DOCKER_ID }} -p ${{ secrets.DOCKER_PASSWORD }}
          docker build -f Dockerfile -t ${{ secrets.DOCKER_REPO }}/goorm .
          docker push ${{ secrets.DOCKER_REPO }}/goorm
      #docker-compose 파일을 ec2 서버에 배포
      - name: Deploy to Prod
        uses: appleboy/ssh-action@master
        id: deploy-prod
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_USERNAME }}
          key: ${{ secrets.EC2_PRIVATE_KEY }}
          envs: GITHUB_SHA
          script: |
             docker stop goorm
             docker rm goorm
             sudo docker pull ${{ secrets.DOCKER_REPO }}/goorm
             docker run -d --name goorm -p 8080:8080 ${{ secrets.DOCKER_REPO }}/goorm
             docker rmi -f $(docker images -f "dangling=true" -q)
    

3-5. Secrets 등록하기

workflow 내의 ${{ ~~ }}는 외부에 공개되어서는 안되는 민감 정보를 시크릿으로 저장해 놓은 것이다.

레포지토리 Settings > Secrets > Actions 에서 등록해주면 된다.

  • DOCKER_ID : Docker-hub 이메일
  • DOCKER_PASSWORD : Docker-hub 비밀번호
  • DOCKER-REPO : Docke-hub Repository 이름
  • EC2_HOST : EC2 public IP address
  • EC2_PRIVATE_KEY : EC2 key pair pem 키 값을 복사한 내용 (*---BEGIN 어쩌구부터 끝까지 복붙해넣어야 한다)
  • EC2_USERNAME : EC2 인스턴스가 ubuntu라면 ubuntu, linux라면 ec2-user
  • PROPERTIES_xxx : properties 파일 내용

이처럼 Github Actions를 이용해 docker에 서버를 올리고, 배포를 자동화하였다.

profile
기록이 주는 즐거움

6개의 댓글

comment-user-thumbnail
2023년 1월 27일

CD Workflow에서
#도커 빌드 & 이미지 push 부분에
goorm 은 도커 아이디를 넣어주면 되나요?

1개의 답글
comment-user-thumbnail
2024년 2월 28일

DOCKER-REPO도 레포지토리명이고 goorm 부분도 레포지토리명인가요??

1개의 답글
comment-user-thumbnail
2024년 7월 17일

안녕하세요 포스팅 감사히 잘 읽었습니다.

질문이 있어 댓글을 남깁니다.

질문1.
RDS 연동을 했음에도 불구하고 docker-compose.yml 에 MySQL 컨테이너 생성을 위한 코드를 작성하신 이유가 궁금합니다.

질문2.
docker-compose.yml 작성하실 때 MYSQL_DATABASE, MYSQL_USER, MYSQL_PASSWORD 값들은 RDS 엔드포인트, 마스터 사용자 이름, 마스터 암호와는 아무 관련이 없는건가요? MySQL 컨테이너에서 데이터베이스에 접속하기 위한 용도일 뿐인 것일까요?

질문3.
배포가 될 때마다 docker-compose 로 앱과 데이터베이스 컨테이너가 계속 새롭게 RUN 된다면 데이터베이스가 날아가는건가요?

1개의 답글