본문 바로가기
노트/F-lab

Github Actions: CI/CD 구축 (feat. Docker)

by soro.k 2023. 6. 21.

CI/CD Process

들어가기 전에

Github Actions와 shell script로 CI/CD를 구축했는데 이번에는 Docker hub에 이미지를 빌드하고 원격 서버에서 이 이미지를 pull 받아서 컨테이너로 실행하도록 했다. 

그래서 밑작업들은 모두 이전 포스팅에 설명해놨기 때문에 설정 안 한 부분이 있다면 위의 링크를 참고하면 좋겠다.

 

참고로 이전 작업들은 Docker를 도입하기 전에 직접 jar 파일을 배포해서 실행하는 shell script도 작성도 해보고 리눅스 명령어도 최대한 경험할 수 있는 시간을 가지기 위함이었다. Docker를 선택한 이유는 개발 스펙 정하기에서도 나눈 적이 있었는데 결국에는 Docker를 사용함으로써 일관성 있는 환경을 제공할 수 있고 이식성 및 확장성과 같은 추가적인 이점들을 누리기 위해서이다. 그리고 Docker compose와 같은 도구로 배포 프로세스를 간소화할 수 있다는 점도 충분히 고려할 사항이라고 생각한다.

 

 

목차

1. Docker hub repository 생성

2. 원격 서버에 Docker 설치

3. CI
  - Dockerfile 생성
  - workflow 생성

4. CD
  - workflow 생성

 

 

시작하기

1️⃣ Docker hub repository 생성

1) Docker hub로그인하고 Create repository 버튼을 클릭한다.

 

2) 정보를 기입하고 Create 버튼을 클릭해 생성한다.

생성된 레포지토리

 

3) workflow에서 활용하기 위해 관련 정보를 secret에 추가한다.

  • Docker hub repository
  • Docker 계정 정보 (username / password)

 

2️⃣ Docker 설치

1) 설치가 가능한 패키지 리스트를 업데이트하고 repository를 사용할 수 있게 패키지를 설치한다.

 sudo apt-get update
 sudo apt-get install ca-certificates curl gnupg

 

2) GPG 키를 추가한다.

 sudo install -m 0755 -d /etc/apt/keyrings
 curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
 sudo chmod a+r /etc/apt/keyrings/docker.gpg

 

3) repository를 설정한다.

 echo \
  "deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
  "$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

 

4) 다시 apt 패키지 리스트를 업데이트하고 최신 버전의 도커를 설치한다.

sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

 

3️⃣ CI

1) Dockerfile을 생성한다.

도커 이미지를 생성하기 위해 Dockerfile을 작성한다. 

FROM openjdk:17.0.1-jdk-slim
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} budsdom.jar
ENTRYPOINT ["java","-jar","/budsdom.jar"]
  • FROM : 이미지를 생성할 때 사용할 기반 이미지
  • ARG : —build-arg 옵션에서 넘길 인자를 정의하기 위해 사용한다. 여기에서는 JAR_FILE을 넘긴다.
  • COPY : 실행할 jar 파일을 도커 컨테이너 내부에 budsdom.jar라는 이름으로 복사한다. 여기서는 프로젝트명으로 정했지만 단순하게 app으로 지정하기도 한다.
  • ENTRYPOINT : 컨테이너가 시작될 때 실행할 스크립트 혹은 명령

 

2) workflow에 Docker 이미지 빌드 과정을 추가한다.

- name: Docker build
  run: |
   docker login -u ${{ secrets.DOCKER_USERNAME}} -p ${{ secrets.DOCKER_PASSWORD }}
   docker build --platform linux/amd64 --build-arg DEPENDENCY=build/dependency -t ${{ secrets.DOCKER_HUB_REPO }} .
   docker tag ${{ secrets.DOCKER_HUB_REPO }} ${{ secrets.DOCKER_USERNAME}}/${{ secrets.DOCKER_HUB_REPO }}:${GITHUB_SHA::7}
   docker push ${{ secrets.DOCKER_USERNAME}}/${{ secrets.DOCKER_HUB_REPO }}:${GITHUB_SHA::7}
  • --platform linux/amd64 : mac m1을 쓰면 linux/arm64로 이미지가 빌드되는데 그러면 원격 서버에서 실행하려고 할 때 문제가 발생하기 때문에 linux/amd64로 설정했다. (mac m1이 아니라면 없어도 됨)
  • ${GITHUB_SHA::7} : 커밋 해시 값으로 이미지의 태그 값을 수동으로 지정해 줄 필요없이 구분되는 값으로 사용할 수 있다. 해시 값의 길이가 길기 때문에 앞의 일곱 글자를 사용한다.

 

4️⃣ CD

1) workflow에 Docker 이미지 pull 및 실행 과정을 추가한다.

만약 CI&CD 과정이 한 workflow 파일에서 관리된다면 아래와 같이 그대로 ${GITHUB_SHA::7}을 사용하면 된다.

- name: Deploy
  uses: appleboy/ssh-action@v0.1.10
  with:
    host: ${{ secrets.SERVER_HOST }}
    username: ${{ secrets.SERVER_USERNAME }}
    key: ${{ secrets.SERVER_PRIVATE_KEY }}
    port: ${{ secrets.SERVER_PORT }}
    script: |
      docker login -u ${{ secrets.DOCKER_USERNAME}} -p ${{ secrets.DOCKER_PASSWORD }}
      docker pull ${{ secrets.DOCKER_USERNAME}}/${{ secrets.DOCKER_HUB_REPO }}:${GITHUB_SHA::7} 
      docker tag ${{ secrets.DOCKER_USERNAME}}/${{ secrets.DOCKER_HUB_REPO }}:${GITHUB_SHA::7} ${{ secrets.DOCKER_HUB_REPO }} 
      docker stop server 
      docker run -d --rm --name server -p 8080:8080 ${{ secrets.DOCKER_HUB_REPO }}

 

그런데 분리되어있다면 해시 값이 변경되기 때문에 추가 작업이 필요하다. 아티팩트를 사용해서 데이터를 유지하는 방법도 있지만 여기서는 간단하게 최신 이미지의 태그 값을 가져오는 명령어를 사용한다. 

- name: Deploy
  uses: appleboy/ssh-action@v0.1.10
  with:
    host: ${{ secrets.SERVER_HOST }}
    username: ${{ secrets.SERVER_USERNAME }}
    key: ${{ secrets.SERVER_PRIVATE_KEY }}
    port: ${{ secrets.SERVER_PORT }}
    script: |
      docker login -u ${{ secrets.DOCKER_USERNAME}} -p ${{ secrets.DOCKER_PASSWORD }}
      DOCKER_TAG=$(docker images ${{ secrets.DOCKER_USERNAME}}/${{ secrets.DOCKER_HUB_REPO }} --format "{{.Tag}} {{.CreatedSince}}" | sort -k2 -r | head -n 1 | awk '{print $1}')
      docker pull ${{ secrets.DOCKER_USERNAME}}/${{ secrets.DOCKER_HUB_REPO }}:${DOCKER_TAG}
      docker tag ${{ secrets.DOCKER_USERNAME}}/${{ secrets.DOCKER_HUB_REPO }}:${DOCKER_TAG} ${{ secrets.DOCKER_HUB_REPO }} 
      docker stop server 
      docker run -d --rm --name server -p 8080:8080 ${{ secrets.DOCKER_HUB_REPO }}

 

 

정리

최종적으로 모든 작업을 적용한 CI/CD workflow는 이렇다.

name: CI

on:
  pull_request:
    branches: [ "main" ]

env:
  YAML_PATH: ./buddy-wisdom-private/src/main/resources/application.yml

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
      - name : Checkout
        uses: actions/checkout@v3
        with:
          token: ${{ secrets.ACCESS_TOKEN }}
          submodules: true

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

      - name: Run chmod to make gradlew executable
        run: chmod +x ./gradlew

      - name: Build with Gradle
        uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1
        with:
          arguments: build

      - name: Docker build
        run: |
          docker login -u ${{ secrets.DOCKER_USERNAME}} -p ${{ secrets.DOCKER_PASSWORD }}
          docker build --platform linux/amd64 --build-arg DEPENDENCY=build/dependency -t ${{ secrets.DOCKER_HUB_REPO }} .
          docker tag ${{ secrets.DOCKER_HUB_REPO }} ${{ secrets.DOCKER_USERNAME}}/${{ secrets.DOCKER_HUB_REPO }}:${GITHUB_SHA::7}
          docker push ${{ secrets.DOCKER_USERNAME}}/${{ secrets.DOCKER_HUB_REPO }}:${GITHUB_SHA::7}

 

 

name: CD

on:
  push:
    branches: [ "main" ]

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v3
        with:
          token: ${{ secrets.ACCESS_TOKEN }}
          submodules: true
          
      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'temurin'

      - name: Run chmod to make gradlew executable
        run: chmod +x ./gradlew

      - name: Build with Gradle
        uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1
        with:
          arguments: build

      - name: Deploy
        uses: appleboy/ssh-action@v0.1.10
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USERNAME }}
          key: ${{ secrets.SERVER_PRIVATE_KEY }}
          port: ${{ secrets.SERVER_PORT }}
          script: |
              docker login -u ${{ secrets.DOCKER_USERNAME}} -p ${{ secrets.DOCKER_PASSWORD }}
              DOCKER_TAG=$(docker images ${{ secrets.DOCKER_USERNAME}}/${{ secrets.DOCKER_HUB_REPO }} --format "{{.Tag}} {{.CreatedSince}}" | sort -k2 -r | head -n 1 | awk '{print $1}')
              docker pull ${{ secrets.DOCKER_USERNAME}}/${{ secrets.DOCKER_HUB_REPO }}:${DOCKER_TAG}
              docker tag ${{ secrets.DOCKER_USERNAME}}/${{ secrets.DOCKER_HUB_REPO }}:${DOCKER_TAG} ${{ secrets.DOCKER_HUB_REPO }} 
              docker stop server 
              docker run -d --rm --name server -p 8080:8080 ${{ secrets.DOCKER_HUB_REPO }}

 

 

참고