Song[coding diary index]

Song 배열에 코딩 흔적 남겨두기

spring

[EP 1-5] 초보 ver. CICD 파이프라인 구축하기!(Github Action, Docker, Spring boot)

singsangssong 2025. 4. 4. 03:12
반응형

시작계기

최근 AI가 나날이 발전함에 따라서 코드만 짜는 백엔드 개발자로는 살아남기 힘들다는 생각이 문득 들기 시작했다.

그래서 '⚙️시스템 아키텍쳐⚙️적인 부분을 아는 백엔드 + Devops를 겸하는 개발자가 비로소 살아남을 수 있지 않을까...' 라는 생각에

이렇게 일단 누구나 다 하는 CICD파이프라인을 구축해보고자 한다.

 

매번 쓰던 CICD를 사용했지만, 하도 쓸모없는 코드가 많아서 최적화시킬 겸 이렇게 글을 작성한다.

내 글을 보고 보충하면 좋을 점과 피드백을 주는 것은 언제나 환영한다.

 

👨‍💻 사용한 기술

  • Github Action -> 파이프라인 구축용
  • Gradle -> Java 전용 빌드 파일 도구
  • Docker -> 이미지 파일 빌드
  • Docker Compose -> 다수의 컨테이너 한번에 띄우기
  • GCP -> 클라우드 인스턴스, VCP등 구축
  • Spring boot -> 서버

좋은 보일러플레이트 코드를 짜보자! 

나중에 되면 다량의 컨테이너를 조작하는 경험도 해보는게 좋지 않을까...?


시작하기 전!

파이프라인을 구성하기 위해서 사용하는 환경변수, 특히 노출되면 안되는 정보들은 모두 github의 secrets를 사용했다.

 

🤫 github secrets에 설정하는 방법

github repository 툴바 -> settings -> secrests and variables -> actions -> repository secrets 

위처럼 설정해두면, 환경변수가 노출되지 않으면서 파이프라인에서 변수를 불러와서 쉽게 사용할 수 있다.

 

🔐 환경변수

  • DOCKER_USERNAME
  • DOCKER_PASSWORD
  • DOCKER_REPO: 도커 레지스트리 이름
  • VM_HOST: 인스턴스 ip주소
  • VM_USERNAME: 인스턴스 접근자(ssh 사용자)
  • VM_SSH_PRIVATE_KEY: ssh private key값. (cat {ssh_key_name}한 값의 전부를 넣어줘야함.)

🌊 기본적인 플로우

1. github repository에 내가 수정한 코드를 push함.

2. push한 브랜치가 github action에 지정된 브랜치라면, 파이프라인 동작시작.

3. 도커파일을 이미지로 빌드한 후, 해당 이미지를 docker registry로 push함.

4. ssh를 이용해서 gcp에 있는 내 서버컴퓨터에 접속함.

5. Docker-compose파일을 실행시켜, 필요한 컨테이너를 모두 실행시킴. (이전에 실행시켰던 컨테이너는 꺼줌.)


코드 분석

Github Actions

아래 더보기는 전체코드다. 하나하나의 단위마다 살펴보면서 알아보자.

더보기
name: CI/CD pipeline

on:
  push:
    branches: [ master, dev ]

permissions:
  contents: read

jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3

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

      - name: make application-dev.yml
        run: |
          cd ./src/main/resources
          touch ./application-dev.yml
          echo "${{ secrets.YML }}" > ./application-dev.yml
        shell: bash

      - name: Upload docker-compose.yml
        run: |
          echo "${{ secrets.VM_SSH_PRIVATE_KEY }}" > private_key.pem
          chmod 600 private_key.pem
          scp -o StrictHostKeyChecking=no -i private_key.pem ./src/main/resources/static/docker-compose.yml ${{ secrets.VM_USERNAME }}@${{ secrets.VM_HOST }}:~/
          rm -f private_key.pem

      - name: DockerFile build & push
        run: |
          docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
          docker build -t ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO }}:latest .
          docker push ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO }}

  deploy:
    needs: build
    runs-on: ubuntu-latest
    name: Deploy
    steps:
      - name: Deploy Docker containers
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.VM_HOST }}
          username: ${{ secrets.VM_USERNAME }}
          key: ${{ secrets.VM_SSH_PRIVATE_KEY }}
          port: 22
          script: |
            set -e
            sudo docker-compose down || true
            sudo docker rm -f $(sudo docker ps -aq) || true
            
            sudo docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
            sudo docker pull ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO }}:latest
            
            cd ~
            sudo docker-compose up -d
            sudo docker image prune -f
on:
  push:
    branches: [ master, dev ]

permissions:
  contents: read

push한 브랜치가 master, dev일 경우, 실행되도록 한다. 이에 github actions가 repsitory의 코드에 읽기 권한을 가진다!

 

jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3

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

GitHub Actions 실행 환경에 현재 저장소 코드를 가져옴. 이후 단계에서 코드가 필요하므로 필수임.

• 이때 java 17을 설치해서 코드를 이해하도록 함.(Temurin JDK 17)

 

      - name: make application-dev.yml
        run: |
          cd ./src/main/resources
          touch ./application-dev.yml
          echo "${{ secrets.YML }}" > ./application-dev.yml
        shell: bash

application-dev.yml을 생성해서 배포용 환경변수를 설정.

secrets.YML 값을 application-dev.yml에 저장.

로컬 개발 환경이 아닌, 배포 환경에서 사용할 설정을 적용.

 

      - name: Upload docker-compose.yml
        run: |
          echo "${{ secrets.VM_SSH_PRIVATE_KEY }}" > private_key.pem
          chmod 600 private_key.pem
          scp -o StrictHostKeyChecking=no -i private_key.pem ./src/main/resources/static/docker-compose.yml ${{ secrets.VM_USERNAME }}@${{ secrets.VM_HOST }}:~/
          rm -f private_key.pem

SSH 키를 생성하고 원격 서버에 docker-compose.yml을 전송.

scp(Secure Copy Protocol)를 사용해 안전하게 파일을 복사.

⚠️ 위 부분은 배포환경의 docker-compose.yml 환경을 바꿀 때, 서버에 직접 접근해서 shell로 바꾸기 번거롭기에, 로컬에서 배포환경을 바꾸고 적용시키기 위한 과정이다.⚠️

 

      - name: DockerFile build & push
        run: |
          docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
          docker build -t ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO }}:latest .
          docker push ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO }}

Docker Hub 로그인

애플리케이션을 Docker 이미지로 빌드

Docker Hub에 최신 이미지 업로드

어플리케이션을 gralde을 활용해서, DockerFile에 작성된 코드를 기반으로 이미지로 빌드시킨다. registry에 빌드한 이미지를 푸시해서, 해당 이미지를 환경에 상관없이 실행될 수 있도록 한다.

 

  deploy:
    needs: build
    runs-on: ubuntu-latest
    name: Deploy
    steps:
      - name: Deploy Docker containers
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.VM_HOST }}
          username: ${{ secrets.VM_USERNAME }}
          key: ${{ secrets.VM_SSH_PRIVATE_KEY }}
          port: 22
          script: |
            set -e
            sudo docker-compose down || true
            sudo docker rm -f $(sudo docker ps -aq) || true
            
            sudo docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
            sudo docker pull ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO }}:latest
            
            cd ~
            sudo docker-compose up -d
            sudo docker image prune -f

host, username, key를 통해서, 서버에 ssh로 접속.

docker-compose를 통해서 컨테이너를 실행.

환경변수들을 통해서 ssh접근으로 서버에서 아래 터미널 명령어를 실행한다.

명령어를 간단하게 말하자면, 기존에 실행되고있던 컨테이너를 모두 종료하고, 새로운 서버 이미지를 가져온 뒤 서버에 필요한 모든 컨테이너를 실행시키는 과정이다.


🐳 DockerFile

FROM gradle:7.6-jdk17 as builder
WORKDIR /build

# 그래들 파일이 변경되었을 때만 새롭게 의존패키지 다운로드 받게함.
COPY build.gradle settings.gradle /build/

# 의존성 미리 다운로드. test는 제외하고, 로그를 모두 무시한채 빌드.
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",                                                 \
    "-Duser.timezone=Asia/Seoul",                           \
    "-Dsun.net.inetaddr.ttl=0",                             \
    "app.jar"              \
]

도커파일에서 직접 빌드과정을 모두 거치는 모습이다.


🐳🐳🐳 docker-compose.yml 🏺🏺🏺

version: '3.8'

services:

  mysql:
    image: mysql:8.0.33
    platform: linux/amd64
    container_name: gdg-mysql
    restart: always
    ports:
      - 13306:3306
    environment:
      MYSQL_DATABASE: gdg_db
      MYSQL_ROOT_PASSWORD: gdg1234
      MYSQL_USER: team4
      MYSQL_PASSWORD: gdg1234
    volumes:
      - ./mysql/data:/var/lib/mysql
      - ./mysql/config:/etc/mysql/conf.d
      - ./mysql/init/schema.sql:/docker-entrypoint-initdb.d/init.sql
    networks:
      - testnet

  backend:
    container_name: gdg-server
    image: singsangssong/gdg_team4
    expose:
      - 8080
    ports:
      - 8080:8080
    restart: always
    depends_on:
      - mysql
    networks:
      - testnet
    environment:
      - SPRING_PROFILES_ACTIVE=dev

networks:
  testnet:
    driver: bridge

mysql과 backend 컨테이너 총 2개를 실행한다.

 

Mysql

MySQL 데이터베이스 초기 설정을 환경 변수로 지정했다.

포트 매핑: 13306:3306호스트의 13306 포트를 컨테이너의 3306 포트(MySQL 기본 포트)로 연결.

볼륨 설정 (데이터 유지):

  • ./mysql/data:/var/lib/mysql → 데이터베이스 데이터를 로컬에 영구 저장.

  • ./mysql/config:/etc/mysql/conf.d → MySQL 설정 파일을 적용할 수 있음.

  • ./mysql/init/schema.sql:/docker-entrypoint-initdb.d/init.sql → 컨테이너 시작 시 실행할 초기 SQL 스크립트 설정.

 

Backend

도커 registry에 등록된 이미지를 사용해 Spring Boot 서버 실행.

포트 설정: 8080:8080호스트와 컨테이너의 8080 포트 연결.

restart: always → 컨테이너가 중단되면 자동 재시작.

depends_on: mysqlMySQL 컨테이너가 먼저 실행된 후에 실행.

Spring Profile 활성화: SPRING_PROFILES_ACTIVE=dev → Spring Boot가 application-dev.yml 설정을 사용하도록 지정.

 

특수한점

1. 추가적으로 필요한 컨테이너(ex: redis, nginx 등)가 있다면, /src/main/resources/static 내부에 있는 docker-compose.yml에 작성하면 곧바로 배포되도록 했다! 😃😃😃😃😃

2. 서버환경변수를 로컬과 배포 버전으로 나누고, 사용자가 환경변수를 신경쓰고 바꿔서 배포하는게 아닌, 자동으로 배포환경에 맞게, 로컬환경에 맞게 실행되도록 분리해두었다! 😃😃😃😃😃

 

트러블 슈팅

도커 컨테이너의 외부/내부 포트 구분하기

mysql과 server간의 db연결이 자꾸 안되는게 문제였다. 기존에는 아래와 같이 작성했었다.

위의 문제점은 2가지이다. (mysql port -> 13306:3306 (외부:내부))

1. mysql:13306: 지정하는 container의 이름이 잘못되었다. mysql -> gdg-mysql

2. 이부분이 중요하다. 만약 도커에서 같은 network에서 컨테이너간의 통신을 수행할때

는 내부포트로 통신해야하고, network 외부, 즉 직접 실행시킨 서버에서는 13306으로 접근해야한다. ✅

2번 설명 그림
이처럼 바꿔주면 해결!

도커파일 Gradle 빌드하기

'Dockerfile에서 직접 빌드하기' 는 문제가 조금 있다...

1. 빌드 속도가 느림 → Docker 이미지 빌드할 때마다 Gradle을 실행해야 해서 시간이 오래 걸림.

2/ 불필요한 리소스 사용 → Docker 빌드가 실패하면 JAR 파일도 생성되지 않으므로, 빠른 검증이 어려움.

3/ Gradle 캐시 문제 → 코드 변경이 없을 경우, 이전 JAR 파일이 유지될 가능성이 있음.

대신 기존 코드대로 하면 환경과 무관하게 빌드할 수 있기는 하다!

 

그래서... cicd에서 빌드한 jar파일을 docker이미지에 그대로 추가하기만 하면 더 최적의 코드를 구현할 수 있다.

$./gradlew clean build 를 통해서 먼저 jar파일로 빌드를 마치고, jar파일을 docker 이미지에 복사하기만 하면 된다..

 

🚨번외: cicd에서 빌드한 jar파일 docker이미지에 추가하기 코드

# github actions: 도커 빌드 전에 수행.
      - name: Build JAR
        run: |
          ./gradlew clean build -x test
        shell: bash
        

# Dockerfile
FROM openjdk:17.0-slim
WORKDIR /app

COPY build/libs/*.jar ./app.jar

EXPOSE 8080

USER nobody
ENTRYPOINT [
    "java",
    "-jar",
    "-Duser.timezone=Asia/Seoul",
    "-Dsun.net.inetaddr.ttl=0",
    "app.jar"
]

 

도커 태그 작성하기

- 도커의 태그(-t)는 무조건 소문자로 작성해야한다.

 

지금까지 cicid파이프라인 구현하기를 읽어주셔서 감사합니다..!

피드백, 충고, 댓글, 조언, 지적 모두 언제나 환영입니다!