Github Action를 활용한 배포 자동화

오형상·2023년 5월 24일
3

도커

목록 보기
4/5
post-thumbnail

저번 프로젝트에서는 GitLab 파이프라인과 Crontab을 활용해 Docker 이미지를 빌드하고 주기적으로 배포하는 구조를 사용했었습니다.
코드 푸시와는 무관하게 일정 주기로 이미지 pull 및 컨테이너 재시작을 수행하도록 구성했지만,
관리 측면에서 다소 복잡하고 불필요한 리소스 사용이 발생할 수 있는 구조였습니다.

이번에는 GitHub Actions를 활용하여,

  • 코드 변경 시 자동으로 빌드
  • 빌드된 이미지를 Docker Hub에 Push
  • EC2 서버에 SSH로 접속해 이미지 Pull 및 배포

하는 방식으로 개선하였습니다.

특히 배포 시에는 Docker Compose를 활용하여 Spring Boot, MySQL, Redis 컨테이너를 통합 관리하였습니다.
이 방식을 선택한 이유는, 하나의 설정 파일로 여러 컨테이너를 동시에 제어할 수 있어 서비스 관리가 용이하고, 확장성도 확보되기 때문입니다.

0. 사전 준비

도커가 설치된 AWS EC2와 Github Repository가 필요합니다.
아래 주소를 참고해 주세요.
1. AWS EC2 서버 띄우기
2. AWS EC2에 도커 설치

1. Docker Repository 생성

dockerhub 사이트에 접속하여 회원가입 및 Repoistory 생성

✨ Repository Name은 꼭 기억해 둘것
create reposiotry

2. Dockerfile 작성

파일 이름 Dockerfile에서 변경 ❌ -> 파일이름을 임의로 변경하면 오류 납니다.

FROM gradle:7.6-jdk17-alpine as builder
WORKDIR /build

# 그래들 파일이 변경되었을 때만 새롭게 의존패키지 다운로드 받게함.
COPY build.gradle settings.gradle /build/
RUN gradle build -x test --parallel --continue > /dev/null 2>&1 || true

# 빌더 이미지에서 애플리케이션 빌드
COPY . /build
RUN gradle build -x test --parallel

# APP
FROM openjdk:17.0-slim
WORKDIR /app

# 빌더 이미지에서 jar 파일만 복사
COPY --from=builder /build/build/libs/*-SNAPSHOT.jar ./app.jar

EXPOSE 8080

# root 대신 nobody 권한으로 실행
USER nobody
ENTRYPOINT [                                                \
    "java",                                                 \
    "-jar",                                                 \
    "-Djava.security.egd=file:/dev/./urandom",              \
    "-Dsun.net.inetaddr.ttl=0",                             \
    "app.jar"              \
]

3. Workflow 작성

이제 github action의 workflow를 작성해보겠습니다.
Github Actions가 무엇을 수행할지를 정의할 수 있습니다.

깃헙 레파지토리에서 Actios에 들어가 새로운 workflow를 생성할 수 있습니다.

아래와 같이 작성 후 Commit

name: CI/CD

on:
  push:
    branches: [ "main" ]

permissions:
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest
    steps:

      - name: checkout
        uses: actions/checkout@v3

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

        ## create application-database.yml
      - name: make application-database.yml
        run: |

          cd ./src/main/resources
          
          # application-database.yml 파일 생성
          touch ./application-database.yml
          
          # GitHub-Actions 에서 설정한 값을 application-database.yml 파일에 쓰기
          echo "${{ secrets.DATABASE }}" >> ./application-database.yml
        shell: bash

      - name: make application-redis.yml
        run: |
          cd ./src/main/resources

          # application-redis.yml 파일 생성
          touch ./application-redis.yml

          # GitHub-Actions 에서 설정한 값을 application-redis.yml 파일에 쓰기
          echo "${{ secrets.REDIS }}" >> ./application-redis.yml
        shell: bash


      - name: Build with Gradle
        run: |
          chmod +x gradlew 
          ./gradlew build
          
#          ./gradlew bootJar
      ## 웹 이미지 빌드 및 도커허브에 push
      - name: web docker build and push
        run: |
          docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
          docker build -t ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO }} . 
          docker push ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO }}
      ## docker compose up
      - name: executing remote ssh commands using password
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.HOST }}
          username: ubuntu
          key: ${{ secrets.KEY }}
          script: |
            sudo docker rm $(sudo docker ps -a)
            sudo docker pull ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO }}
            cd ~
            docker-compose up -d
            sudo docker image prune -f
            

👀 참고) ./gradlew build과 ./gradlew bootJar 차이

  • gradle build

gradle build는 bootJar를 포함하고 있어 내부 동작이 더 길다.

build는 test 코드가 있다면 테스트도 수행을 한다.

build와 bootJar가 다른 것 중 대표적인 것은 Life Cycle과 관련된 Task가 존재한다.
예를 들어 check, assemble가 있다.

  • gradle bootJar

단순히 프로젝트의 jar 파일만을 만드는데 목적을 가지고 있다.

그만큼 빌드하는데 속도도 빠를 것.

4. Github Secrets 설정

깃헙 레파지토리 -> Seetings -> Secrets and variables -> Actions -> new repository secret 에서 변수 설정이 가능합니다.

database & redis

깃헙에 올리지 못하는 db정보들을 저장할 변수입니다.
예시로 database는 아래와 같습니다.

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql:/<AWS EC2 public IP>/<DB 스키마>
    username: root
    password: 1q2w3e4r
  jpa:
    database: mysql  
    database-platform: org.hibernate.dialect.MySQLDialect
    hibernate:
      ddl-auto: update
    properties:
      hibernate:
        format_sql: true

참고로 application.yml은 아래와 같이 올라가있습니다. 프로젝트를 돌리면 application-database.yml와 application-redis.yml를 찾아서 설정정보를 참고하게됩니다.

spring:
  profiles:
    include: 
    	- mysql
        - redis

DOCKER_USERNAME & DOCKER_PASSWORD & DOCKER_REPO

  • DOCKER_USERNAME = docker hub 아이디
  • DOCKER_PASSWORD = docker hub 비밀번호
  • DOCKER_REPO = 1.에서 만든 docker repository 이름

HOST & KEY

  • HOST = AWS EC2 public Ip
  • KEY = AWS EC2 생성시 사용한 pem 키
    ✨ -----BEGIN RSA PRIVATE KEY----- 부터 -----END RSA PRIVATE KEY----- 부분을 포함

❓ pem key 확인 방법

  • windows : vscode, intellij 등 편집기를 사용하여 확인 가능
  • mac : 터미널에서 cat <pem 키 경로>로 확인 가능

5. docker-compose 설치

sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

# 선택 사항 : PermissionError: [Errno 13] Permission denied 발생하면 적어주세요.
sudo chmod 666 /var/run/docker.sock

docker-compose --version 통해 설치가 잘 되었는지 확인합니다.

6. docker-compose.yml 작성

# 홈 디렉토리로 이동
cd ~

# docker-compose.yml 파일을 생성
sudo vi docker-compose.yml

👀 vi 편집기 사용법 (자세한 내용은 구글링)

i = 입력 모드
esc = 명령어 모드
:wq = 저장 후 종료

docker-compose.yml

version: '3.8'

services:

  mysql:
    container_name: mysql
    image: mysql
    ports:
      - 3306:3306
    environment:
      MYSQL_ROOT_PASSWORD: 1q2w3e4r
      MYSQL_DATABASE: shoppingmall
    restart: always
    networks:
      - testnet
  redis:
    container_name: redis
    image: redis
    ports:
      - 6379:6379
    restart: always
    networks:
      - testnet

  backend:
    container_name: backend
    image: zvyg1023/shopping-mall
    expose:
      - 8080
    ports:
      - 8080:8080
    restart: always
    depends_on:
      - mysql
      - redis
    networks:
      - testnet


networks:
  mynet:
    driver: bridge
    

7. 동작 확인하기

우선 워크플로우는 성공적으로 마쳤습니다.

docker ps를 통해 컨테이너가 돌아가는지 확인해보겠습니다.

마지막으로 배포된 주소로 스웨거 접속 해보겠습니다.

트러블 슈팅

docker-compose를 사용하여 Spring과 MySQL을 배포했으나 Spring에서 MySQL과 연결이 되지 않아서 exception이 발생하며 Spring application이 종료되는 문제 발생하였다.

Spring이 MYSQL에 종속적이라 depends_on 옵션을 사용하여 작업 순서를 보장해주었지만 여진히 작동하지 않았다.

docker-compose up의 로그를 확인 해 보니 MySQL에서 ready for connections 상태가 되기 전에 Spring이 먼저 실행되며 DB connection을 시도하였다는 것을 알 수 있었다.

최종적으로 restart 옵션을 주어서 해결하였지만 맞는 방법인지는 모르겠다.


최종 배포 흐름


전체 코드 보러가기

0개의 댓글

관련 채용 정보