[프로젝트 배포] Docker를 활용한 배포 및 CI/CD 구축

승등·2024년 6월 6일

배포

목록 보기
3/3
post-thumbnail

앞선 포스트에서는 배포를 위한 EC2,RDS 인스턴스와 S3 버킷 생성과 관련 설정까지 완료하였다.
그 후 프로젝트를 직접 배포하기 위해 git clone 방식을 우선 시도 한 후, jar 파일을 FTP를 사용하여 직접 전달하는 방법으로 배포를 완료하였다.

이번 포스트에서는 CI/CD 파이프라인을 구축하기 위해 Github Actions와 Docker를 활용 해볼 것이며 이를 위해 Docker를 사용해 프로젝트를 배포하는 방법을 확인 해볼 것이다.


Docker를 활용한 배포

흐름

도커를 활용한 배포 흐름은 간략하게 아래와 같다.

  1. Dockerfile을 작성하여 build 시 docker image가 생성된다.
  2. 생성된 docker image를 docker hub에 push한다.
  3. 어플리케이션을 실행할 서버(EC2)에서 docker hub의 docker image를 pull한다.
  4. docker image를 실행시켜 docker container를 생성한다.(어플리케이션은 container상에서 실행된다.)

사전준비

로컬 환경에 Docker를 설치한다.
OS별 Docker 설치 방법 참고

Docker Hub 리포지토리 생성

Docker Hub 에 docker image를 push할 리포지토리를 생성한다.
리포지토리 이름과 설명을 작성한 후 Public으로 생성한다. (Private의 경우 계정 당 1개까지만 무료로 생성이 가능하다.)

Dockerfile 생성

프로젝트의 최상위 경로 아래에 Dockerfile을 생성한다. (※ src 아래에 생성하지 않도록 주의)

Dockerfile 작성을 위해 다음과 같은 지시어가 사용된다.

지시어설명
FROM베이스 이미지 지정
RUN이미지를 지정하면서 실행할 명령 지정
ENTRYPOINT컨테이너의 어플 지정 (컨테이너 시작할 때 실행할 명령어)
EXPOSE컨테이너의 포트 지정
ADD이미지 생성 시 파일 추가
COPY이미지 생성시 파일 복사
WORKDIR컨테이너 작업 디렉토리 지정
MAINTAINER이미지 작성자 명시
CMD컨테이너의 어플 지정 (컨테이너 시작할 때 실행할 명령어)
LABEL이미지의 라벨 지정
ENV컨테이너의 환경 변수 지정
VOLUME컨테이너의 볼륨 지정
USER컨테이너의 사용자 지정
ARG인자 설정

Dockerfile

# jdk 17(amazoncorretto:17) 환경으로 구성
FROM amazoncorretto:17

# 인자 설정 :: 변수명 JAR_FILE
# build/libs(빌드 시 jar파일 생성 경로) 하위의 모든 jar파일
ARG JAR_FILE=build/libs/*.jar

# Docker Image 생성 시 JAR_FILE을 app.jar로 복사
COPY ${JAR_FILE} app.jar

# 실행 명령어
ENTRYPOINT ["java", "-jar", "app.jar"]

gradle build

./gradlew clean build

프로젝트 경로의 터미널에서 build하여 jar파일을 생성한다.

Docker Image Push

Docker Image를 생성해서 Docker Hub에 push 해줄 것이다.

만약 Docker에 로그인되어 있지 않다면 아래와 같은 방법으로 로그인 해주자.

docker login -u [username]
# 이후 password 입력

이미지 생성

# 이미지 생성(build) 명령어
# (태그 생략 시 기본 :latest로 지정)
docker build -t [도커 허브 ID]/[이미지명]:[태그명] [Dockerfile위치]

# 실제 작성
# . (현재 위치)
docker build -t dulledulle/dulle-dulle-springboot .

맥북의 경우 명령어에 --platform linux/amd64를 추가해줘야 한다.

생성된 이미지는 docker images 명령어로 목록을 확인 할 수 있다.

Docker Hub로 Push

# push 명령어
docker push [도커 허브 ID]/[이미지명]

# 실제 작성
docker push dulledulle/dulle-dulle-springboot

리포지토리의 Tags를 확인해보면 정상적으로 push 됐음을 확인 할 수 있다.

AWS EC2에서 Pull

앞서 생성한 EC2 인스턴스의 서버(ubuntu)에서 Docker Hub에 push된 이미지를 push 한다.

Docker 설치

# 도커 설치
sudo apt-get install docker.io -y

# docker 서비스 실행
sudo service docker start

# /var/run/docker.sock 파일 권한 변경, 그룹 내 다른 사용자도 접근 가능하게 변경
sudo chmod 666 /var/run/docker.sock

# ubuntu 유저를 docker 그룹에 추가
sudo usermod -a -G docker ubuntu

Docker Image Pull

sudo docker pull [도커 허브 ID]/[이미지명]

Docker Container 실행

sudo docker run -d -p [로컬 port]:[도커 port] [도커 허브 ID]/[이미지명]
  • -d (--detach) : 백그라운드 실행
  • -p : 실행할 포트 설정

CI/CD

이번에는 위의 과정 (build - docker image 생성 - docker hub로 push - 서버에서 pull - container 실행) 을 자동화하여 변경사항을 commit/push 할 시 서버에 배포까지 한번에 이루어지도록 CI/CD 파이프라인을 구축해보자.

이번 프로젝트의 CI/CD 파이프라인 구축에는 Github Actions를 활용할 것이다.

Github Actions란 Github 저장소를 기반으로 소프트웨어 개발 Workflow를 자동화 할 수 있는 도구다. 특정한 브랜치에 대한 이벤트(트리거)를 통해 설정된 Workflow를 따라 빌드, 테스트, 배포 등의 다양한 동작을 자동으로 실행되게 할 수 있다.

Github Actions의 Secrets로 환경변수 관리

프로젝트 리포지토리의 Settings-Security-Secrets and Variables -Actions 경로에서 보안상의 이유로 Github에 올릴 수 없는 환경변수들을 작성해준다.

Secrets는 Name-Secret(Key-Value)의 형태로 변수 값들을 관리하며 Name은 임의로 작성해줄 수 있다.

  • APPLICATION : application.properties 파일의 내용
  • AWS_EC2_HOST : EC2 인스턴스의 public ip
  • AWS_EC2_KEY : 발급 받은 pem 파일
  • AWS_USERNAME : 인스턴스 생성 시 선택한 OS의 기본 사용자 이름
  • DOCKER_USERNAME : Docker Hub 로그인 시 사용하는 username
  • DOCKER_PASSWORD : Docker Hub 로그인 시 사용하는 password

Workflow 파일 생성

프로젝트 리포지토리의 Actions-New Workflow 를 통해 workflow 파일 작성을 위한 양식을 선택할 수 있다.

'Java with Gradle' 을 선택하면 Java-Gradle 환경에서 사용할 수 있는 workflow 파일의 기본양식을 보여준다. 파일의 이름을 설정하여 생성해준 후 내용을 원하는 workflow에 맞춰 수정한다.

Workflow 파일 작성

Workflow 파일을 생성한 뒤 프로젝트를 update하면 프로젝트 최상단에 .github - workflows - <설정한 파일 이름>.yml 이 생성되어있다.

이 파일을 아래와 같이 수정해주었다.

dulle-dulle-ci-setting.yml

# Github Actions에 표시되는 Workflow 이름
name: Dulle-Dulle CI/CD

# master 브랜치로 push, pull request가 발생하면 workflow를 실행
on:
  push:
    branches: [ "master" ]
  pull_request:
    branches: [ "master" ]

permissions:
  contents: read

# workflow에서 실행할 동작
jobs:
  CI:
    runs-on: ubuntu-latest

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

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

    # application.properties 파일 생성
    - name: Make application.properties
      run: mkdir ./src/main/resources |
           touch ./src/main/resources/application.properties

    - name: Deliver application.properties
      run: echo "${{ secrets.APPLICATION }}" > ./src/main/resources/application.properties

    # Gradle 설정 및 Build
    - name: Setup Gradle
      uses: gradle/actions/setup-gradle@v3

    - name: Change gradlew permissions
      run: chmod +x ./gradlew

    - name: Build with Gradle Wrapper
      run: ./gradlew clean build

    # Docker 로그인
    - name: Docker Login
      uses: docker/login-action@v3
      with:
        username: ${{ secrets.DOCKER_USERNAME }}
        password: ${{ secrets.DOCKER_PASSWORD }}

    # Docker 이미지 생성 및 Push
    - name: Docker build & Push
      run: |
        docker build -t ${{ secrets.DOCKER_USERNAME }}/dulle-dulle-springboot .
        docker push ${{ secrets.DOCKER_USERNAME }}/dulle-dulle-springboot

  CD:
    needs: CI
    runs-on: ubuntu-latest

    steps:
      # EC2 접근 후 docker 이미지 pull & run
      - name: Deploy to EC2 Server
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.AWS_EC2_HOST }}
          username: ${{ secrets.AWS_USERNAME }}
          key: ${{ secrets.AWS_EC2_KEY }}
          port: 22
          script: |
            sudo docker pull ${{ secrets.DOCKER_USERNAME }}/dulle-dulle-springboot
            sudo docker stop $(sudo docker ps -qa)
            sudo docker rm $(sudo docker ps -qa)
            sudo docker run -d -p 8080:8080 \
            -v /etc/localtime:/etc/localtime:ro \
            -e TZ=Asia/Seoul \
            ${{ secrets.DOCKER_USERNAME }}/dulle-dulle-springboot
            sudo docker system prune -f

작성한 파일을 하나씩 확인해보자.

설정

# Github Actions에 표시되는 Workflow 이름
name: Dulle-Dulle CI/CD

# master 브랜치로 push, pull request가 발생하면 workflow를 실행
on:
  push:
    branches: [ "master" ]
  pull_request:
    branches: [ "master" ]

permissions:
  contents: read
  • name : Github Actions에 표시되는 Workflow 이름이다. 파일 이름과 상관없이 자유롭게 작성할 수 있다.
  • on : Workflow의 트리거가 될 이벤트를 설정한다.이 파일에서는 master 브랜치로 push, pull request가 발생하면 workflow를 실행하도록 작성되었다.
  • permissions : Workflow 작업에 적용되는 권한을 설정한다. 권한에 대한 자세한 내용은 공식 문서 참조.

jobs

job은 Workflow의 실행 단위이다. 각 job은 다시 step으로 순차적인 실행 단계가 구분된다.

이번 Workflow에서는 CI와 CD로 job을 나누어 작성하였다.

CI

  CI:
    runs-on: ubuntu-latest

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

    # 빌드 시간 향상을 위한 Gradle 캐싱
    - name: Gradle Caching
      uses: actions/cache@v4
      with:
        path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
        key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
        restore-keys: |
            ${{ runner.os }}-gradle-
  • runs-on : Workflow를 실제로 실행하는 서버(Runner)를 설정한다. GitHub-hosted runner(Github에서 자체적으로 제공)/ Self-hosted runner(사용자가 직접 설정하여 사용) 으로 나뉜다. 여기서는 GitHub-hosted runner를 사용하였다.
  • Set up JDK 17 : 프로젝트와 동일한 JDK 17을 사용하도록 설정하였다.
  • Gradle Caching : 빌드 시간 향상을 위해 Gradle을 캐싱하도록 했다. 작성하지 않아도 Workflow를 실행하는데 문제는 없다.
 # application.properties 파일 생성
    - name: Make application.properties
      run: mkdir ./src/main/resources |
           touch ./src/main/resources/application.properties

    - name: Deliver application.properties
      run: echo "${{ secrets.APPLICATION }}" > ./src/main/resources/application.properties
  • Make application.properties : 보안상의 이유로 Github Repository에서 제외된 application.properties 을 생성한다.
  • Deliver application.properties : 생성된 application.properties에 Secrets에 등록한 APPLICATION의 값을 덮어씌운다.
    # Gradle 설정 및 Build
    - name: Setup Gradle
      uses: gradle/actions/setup-gradle@v3

    - name: Change gradlew permissions
      run: chmod +x ./gradlew

    - name: Build with Gradle Wrapper
      run: ./gradlew clean build
  • Setup Gradle ~ : Gradle을 설정하고 권한 변경 후 프로젝트를 clean build 한다.
    # Docker 로그인
    - name: Docker Login
      uses: docker/login-action@v3
      with:
        username: ${{ secrets.DOCKER_USERNAME }}
        password: ${{ secrets.DOCKER_PASSWORD }}

    # Docker 이미지 생성 및 Push
    - name: Docker build & Push
      run: |
        docker build -t ${{ secrets.DOCKER_USERNAME }}/dulle-dulle-springboot .
        docker push ${{ secrets.DOCKER_USERNAME }}/dulle-dulle-springboot
  • Docker Login : Secrets에 등록된 DOCKER_USERNAME과 DOCKER_PASSWORD로 Docker에 로그인한다.
  • Docker build & Push : Docker Image를 build 하여 생성 후 Docker Hub에 프로젝트를 push한다.

CD

  CD:
    needs: CI
    runs-on: ubuntu-latest

    steps:
      # EC2 접근 후 docker 이미지 pull & run
      - name: Deploy to EC2 Server
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.AWS_EC2_HOST }}
          username: ${{ secrets.AWS_USERNAME }}
          key: ${{ secrets.AWS_EC2_KEY }}
          port: 22
          script: |
            sudo docker pull ${{ secrets.DOCKER_USERNAME }}/dulle-dulle-springboot
            sudo docker stop $(sudo docker ps -qa)
            sudo docker rm $(sudo docker ps -qa)
            sudo docker run -d -p 8080:8080 \
            -v /etc/localtime:/etc/localtime:ro \
            -e TZ=Asia/Seoul \
            ${{ secrets.DOCKER_USERNAME }}/dulle-dulle-springboot
            sudo docker system prune -f
  • needs : 앞선 작업(CI)가 완료 된 후 실행한다.
  • Deploy to EC2 Server : EC2 서버에서 Docker Image를 pull하여 실행하는 작업을 수행한다.

    uses : 원격 접속을 위해 appleboy 를 사용한다.
    script :
    Docker Hub에서 최신 Image를 pull 한 후 실행 중인 컨테이너를 중지, 삭제한다.
    타임존을 한국시간대로 설정하여 image를 run하여 컨테이너를 실행시킨다.
    마지막으로 불필요한 오래된 image는 삭제한다.

    추가(2024.06.10)
    Docker Container의 Timezone 설정을 위해 run 명령어에 아래 옵션을 추가해주었다.

    ...
    -v /etc/localtime:/etc/localtime:ro \
    -e TZ=Asia/Seoul \
    ...

결과 확인

Workflow 파일을 모두 작성 한 후 프로젝트를 push하여 정상적으로 동작하는지 확인해보았다.

배포까지 문제 없이 동작하였다.


레퍼런스

Github Actions과 Docker을 활용한 CI/CD 구축
Docker + Github Actions로 SpringBoot CI/CD 구축하기
[Infra] Github Actions + Docker를 이용한 자동 배포

profile
기록, 코딩보다 어렵다-!

1개의 댓글

comment-user-thumbnail
2024년 6월 6일

Docker에 대해 궁금했는데 유익한 포스팅 감사합니다^^

답글 달기