본문 바로가기

배운내용 정리

Github Actions + Docker compose 버전관리 + nginx

Github Actions + Docker compose를 통해 버전을 관리하려고 한다

Github Actions의 tag 기능을 통해 Docker compose로 버전을 넘겨서 컨테이너로 버전을 적용시키는 방법을 사용해보려고 한다.

버전관리의 이점은 다음과 같다.

 

  1. 명확한 버전 관리:
    • 각 릴리스마다 고유한 태그를 할당하여, 특정 버전의 소스 코드에 쉽게 접근할 수 있다.
    • 버전 태그를 통해 배포된 버전과 코드를 쉽게 추적할 수 있다.
  2. 자동화된 배포:
    • GitHub Actions와 함께 사용하면, 특정 태그가 생성될 때 자동으로 배포 파이프라인을 트리거할 수 있다.
    • EX) v1.0.0 태그가 생성되면 해당 태그에 맞는 배포 작업이 자동으로 실행되도록 설정할 수 있다.
  3. 릴리스 관리 용이:
    • 태그를 사용하여 릴리스를 관리하면, 릴리스 노트를 쉽게 작성하고 관리할 수 있다.
    • 각 태그에 대해 변경 사항, 버그 수정, 새로운 기능 등을 문서화하여 릴리스 히스토리를 체계적으로 관리할 수 있다.
  4. 안정성 보장:
    • 태그를 통해 특정 버전의 안정적인 코드를 보장할 수 있다.
    • 프로덕션 환경에서는 항상 안정성이 검증된 태그를 사용하여 배포함으로써, 불안정한 코드가 배포되는 것을 방지할 수 있다.
  5. 협업 효율성 향상:
    • 팀원 간에 버전 태그를 공유함으로써, 현재 작업 중인 버전과 릴리스 된 버전을 명확히 구분할 수 있다.
    • 이는 코드 리뷰, 버그 수정, 피드백 등 다양한 협업 과정에서 효율성을 높여준다.
  6. 이전 버전 롤백 용이:
    • 문제가 발생했을 때, 이전에 태그 된 안정적인 버전으로 쉽게 롤백할 수 있다.
    • 이를 통해 서비스 중단 시간을 최소화하고 빠르게 대응할 수 있다.
  7. CI/CD 파이프라인 단순화:
    • 태그를 기준으로 CI/CD 파이프라인을 구성하면, 각 단계에서 어떤 코드가 실행되고 배포되는지 명확하게 관리할 수 있다.
    • 이는 배포 파이프라인의 복잡성을 줄이고, 관리 및 유지보수를 쉽게 한다.

구연과정은 다음과 같다.

1. Github Actions workflow 작성

2. docker-compose.yml 작성

3. Dockerfile작성

4. nginx.conf 수정

5. nginx 환경변수 적용을 위한 shell 스크립트 작성

 

1. Github Actions workflow 

TAG 기능을 사용하기 위해 Github Action에서 제공하는 Tag 기능을 사용하여 업로드할 컨테이너에 버전정보를 기입하여  유동성 있게 관리하려고 한다.

먼저 tag를 통해 버전 릴리즈를 올리면 Gtihub Actions에서 CI/CD가 동작하게 끔 설정해 준다.

# 워크 플로우가 언제 실행 될지를 정한다.
on:
  push:
    tags:
      - 'v*'

 

GTIHUB_REF의 환경변수는 Github Actions가 자동으로 설정해 주는 변수이다. 예를 들어 v1.0.0으로 push 했다면 환경변수에는 refs/tags/v1.0.0의 값을 가진다. 이후 sed의 명령어를 통해 refs/tags/ 부분을 제거하고 순수 태그정보를 추출한다.

추출한 정보를 통해 back.env의 환경변수 파일에 VERSION=v1.0.0의 형태로 저장된다.

git tag v1.0.0
git push origin v1.0.0
VERSION=v1.0.0

그다음 GitHub Actions 아티팩트로 업로드 후 CI를 마무리한다.

 

      - name: TAG to back.env file
        run: echo VERSION=$(echo $GITHUB_REF | sed 's/refs\/tags\///') > back.env

      - name: Upload back.env file
        uses: actions/upload-artifact@v2
        with:
          name: back-env
          path: back.env

CD부분도 마찬가지로 서버로 전송할 env파일을 GitHub Actions 아티팩트에서 env파일에서 VERSION에 대한 정보를 추출하여 파일을 전송할 때 파일명에 version에 대한 정보를 추가하여 서버로 전송한다. 

export $(grep VERSION back.env) 명령어를 통해 서버에서 바로 VERSION에 대한 환경변수를 등록한다.

      - name: Deploy to EC2 and Build/Run Docker Containers
        run: |
          echo "${{ secrets.SSH_PEM_KEY }}" > ssh_key.pem
          chmod 600 ssh_key.pem
          VERSION=$(grep VERSION back.env | cut -d '=' -f2)
          scp -i ssh_key.pem -o StrictHostKeyChecking=no build/libs/yours-service-back-0.0.1-SNAPSHOT.jar ${{ secrets.USER }}@${{ secrets.HOST }}:/home/${{ secrets.USER }}/your-service-back-$VERSION.jar
          ssh -i ssh_key.pem -o StrictHostKeyChecking=no ${{ secrets.USER }}@${{ secrets.HOST }} "pgrep java | xargs kill -9; nohup java -jar /home/${{ secrets.USER }}/your-service-back-$VERSION.jar > app.log 2>&1 &"
          scp -i ssh_key.pem -o StrictHostKeyChecking=no back.env ${{ secrets.USER }}@${{ secrets.HOST }}:/home/${{ secrets.USER }}/back.env
          scp -i ssh_key.pem -o StrictHostKeyChecking=no Dockerfile ${{ secrets.USER }}@${{ secrets.HOST }}:/home/${{ secrets.USER }}/Dockerfile
          scp -i ssh_key.pem -o StrictHostKeyChecking=no docker/docker-compose.yml ${{ secrets.USER }}@${{ secrets.HOST }}:/home/${{ secrets.USER }}/docker-compose.yml
          ssh -i ssh_key.pem -o StrictHostKeyChecking=no ${{ secrets.USER }}@${{ secrets.HOST }} << EOF
            cd /home/${{ secrets.USER }}
               export $(grep VERSION back.env)
               docker compose build
               docker compose up -d
          EOF
# 워크 플로우 이름
name: Java CI with Gradle 


# 워크 플로우가 언제 실행 될지를 정한다.
on:
  push:
    tags:
      - 'v*'

      # 워크 플로우가 깃 레포에 대한 권한을 읽기 만 가능하게 설정한다.
permissions:
  contents: read


# 워크플로우에서 할 작업 정의한다.
jobs:
  # 작업 환경 = 우분투 최신 버전
  build:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgis/postgis:16-3.4
        env:
          POSTGRES_PASSWORD: ${{ secrets.DB_PASSWORD }}
          POSTGRES_DB: ${{ secrets.DB_NAME }}
          POSTGRES_USER: ${{ secrets.DB_USER }}
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
      memcached:
        image: memcached:1.6.25
        ports:
          - 11211:11211

          # 깃허브에서 제공하는 checkout 엑션 사용
    steps:
      - uses: actions/checkout@v4

      # JDK 21 설정한당
      # temurin = Adoptium에서 제공하는 JDK
      - name: Set up JDK 21
        uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'temurin'

      # gradle wrapper 파일에 실행 권한을 부여
      # gradle wrapper = 개발자가 특정 버전의 Gradle을 미리 설치하지 않고도 Gradle 빌드를 실행할 수 있게 해주는 편리한 도구
      - name: Grant execute permission for gradlew
        run: chmod +x gradlew

      # 데이터베이스 서비스 준비 대기
      - name: Wait for Postgres to become ready
        run: |
          until docker exec $(docker ps -q -f "ancestor=postgis/postgis:16-3.4") pg_isready -h localhost -p 5432 -U postgres; do
            echo "Waiting for Postgres to become ready..."
            sleep 5
          done
          echo "Postgres is ready."

      # Gradle 빌드 엑션을 이용해서 프로젝트 빌드
      - name: Build with Gradle
        uses: gradle/gradle-build-action@v2
        with:
          gradle-version: '8.4'
          arguments: build test


      - name: TAG to back.env file
        run: echo VERSION=$(echo $GITHUB_REF | sed 's/refs\/tags\///') > back.env

      - name: Upload back.env file
        uses: actions/upload-artifact@v2
        with:
          name: back-env
          path: back.env


      # 빌드해서 생긴 JAR 파일을 깃허브 아티팩트로 업로드!!
      - name: Upload build artifact
        uses: actions/upload-artifact@v2
        with:
          name: your-service-back
          path: build/libs/your-service-back-0.0.1-SNAPSHOT.jar


      - name: Upload Dockerfile
        uses: actions/upload-artifact@v2
        with:
          name: your-service-dockerfile
          path: Dockerfile

      - name: Upload docker-compose
        uses: actions/upload-artifact@v2
        with:
          name: your-service-docker-compose
          path: docker/docker-compose.yml


  # 배포 **
  deploy:
    needs: build
    runs-on: ubuntu-latest

    # 위의 빌드작업한 JAR 파일 = 아티팩트를 다운로드
    steps:
      - name: Download build artifact
        uses: actions/download-artifact@v2
        with:
          name: your-service
          path: build/libs/

      - name: Download back.env file
        uses: actions/download-artifact@v2
        with:
          name: back-env
          path: ./

      - name: Download Dockerfile
        uses: actions/download-artifact@v2
        with:
          name: your-service-dockerfile
          path: ./

      - name: Download docker-compose
        uses: actions/download-artifact@v2
        with:
          name: your-service-docker-compose
          path: docker/

      - name: Deploy to EC2 and Build/Run Docker Containers
        run: |
          echo "${{ secrets.SSH_PEM_KEY }}" > ssh_key.pem
          chmod 600 ssh_key.pem
          VERSION=$(grep VERSION back.env | cut -d '=' -f2)
          scp -i ssh_key.pem -o StrictHostKeyChecking=no build/libs/yours-service-back-0.0.1-SNAPSHOT.jar ${{ secrets.USER }}@${{ secrets.HOST }}:/home/${{ secrets.USER }}/your-service-back-$VERSION.jar
          ssh -i ssh_key.pem -o StrictHostKeyChecking=no ${{ secrets.USER }}@${{ secrets.HOST }} "pgrep java | xargs kill -9; nohup java -jar /home/${{ secrets.USER }}/your-service-back-$VERSION.jar > app.log 2>&1 &"
          scp -i ssh_key.pem -o StrictHostKeyChecking=no back.env ${{ secrets.USER }}@${{ secrets.HOST }}:/home/${{ secrets.USER }}/back.env
          scp -i ssh_key.pem -o StrictHostKeyChecking=no Dockerfile ${{ secrets.USER }}@${{ secrets.HOST }}:/home/${{ secrets.USER }}/Dockerfile
          scp -i ssh_key.pem -o StrictHostKeyChecking=no docker/docker-compose.yml ${{ secrets.USER }}@${{ secrets.HOST }}:/home/${{ secrets.USER }}/docker-compose.yml
          ssh -i ssh_key.pem -o StrictHostKeyChecking=no ${{ secrets.USER }}@${{ secrets.HOST }} << EOF
            cd /home/${{ secrets.USER }}
               export $(grep VERSION back.env)
               docker compose build
               docker compose up -d
          EOF

 

2. docker-compose.yml

Github Actions를 통해 등록한 환경변수를 통해 docker-compose.yml에서 사용한다.

주의할 점은 Dockerfile을 사용 중이라면 환경변수 등록을 하기 위해서는 아래의 과정이 필요하다. 환경변수 등록을 통해 Dockerfile에서 변수 사용이 가능해진다.

      args:
        JAR_FILE: your-service-${VERSION}.jar

환경변수를 등록했다면 도커의 이미지 및 컨테이너에 환경변수 적용이 가능하다.

services:  
  app:
    env_file:
      - ./back.env
    build:
      context: .
      dockerfile: Dockerfile
      args:
        JAR_FILE: your-service-${VERSION}.jar
    image: your-service-${VERSION}
    ports:
      - "8080:8080"
    container_name: your-service-api-${VERSION}
    restart: unless-stopped
    depends_on:
      - postgresql
      - memcached
    environment:
      - SPRING_PROFILES_ACTIVE=prod
      - DATABASE_HOST=postgresql
      - MEMCACHED_HOST=memcached
      - MEMCACHED_PORT=11211
    networks:
      - app-network


  nginx:
    image: nginx:latest
    restart: unless-stopped
    env_file:
      - ./back.env
    volumes:
      - ./conf/nginx.conf:/etc/nginx/nginx.conf
      - ./start-nginx.sh:/usr/local/bin/start-nginx.sh
      - ./data/certbot/conf:/etc/letsencrypt
      - ./data/certbot/www:/var/www/certbot
      - ./build:/usr/share/nginx/html
      - /usr/share/zoneinfo/Asia/Seoul:/etc/localtime:ro
    ports:
      - "80:80"
      - "443:443"
    command: ["/usr/local/bin/start-nginx.sh"]
    depends_on:
      - app
      - postgresql
    networks:
      - app-network
      
networks:
  app-network:
    driver: bridge

 

3. Dockerfile

서비스 컨테이너의 Dockerfile은 다음과 같다. docker-compose.yml에서 설정한 환경변수를 바탕으로 Dockerfile에도 똑같이 적용

FROM openjdk:21-jdk-slim

# 시간대 설정
ENV TZ=Asia/Seoul
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

RUN mkdir /app

# EC2 인스턴스에 이미 존재하는 JAR 파일을 이미지에 복사
ARG JAR_FILE
COPY ${JAR_FILE} /app/your-service.jar

# 컨테이너 실행 시 JAR 파일 실행
ENTRYPOINT ["java", "-jar", "/app/your-service.jar"]

 

4. nginx.conf 수정

proxy_pass는 Nginx가 클라이언트의 요청을 백엔드 서버로 전달할 때 사용하는 URL을 지정하는데 이 URL은 Docker 네트워크 내에서의 컨테이너 이름을 사용하여 지정된다.

Nginx는 기본적으로 환경변수를 지원하지 않는다. 따라서 환경변수를 적용하기 위해서는 여러 방법이 존재하는데 docker-compose.yml파일에서 environment를 정의해서 적용하는 방법도 있지만 실제로 해본 결과 적용되지 않았다. 그 이유는 nginx.conf 파일이 정적파일이기 때문에 파싱 할 때 환경변수를 적용하지 않는다. 따라서 해결 방법으로는 shell 스크립트를 사용해서 동적으로 nginx.conf를 생성하는 방법이 있다.

events{
    worker_connections  4096;
}

http{
  server {
    listen 80;
    server_name ${your_domain};
    server_tokens off;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
        allow all;
    }

    location / {
        return 301 https://$host$request_uri;
    }
  }

  server {
    listen 443 ssl;
    server_name ${your_domain};
    server_tokens off;

    ssl_certificate /etc/letsencrypt/live/${your_domain}/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/${your_domain}/privkey.pem;

    include /etc/letsencrypt/options-ssl-nginx.conf; # Let's Encrypt에서 제공하는 SSL 설정
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # Diffie-Hellman group

    location / {
    	proxy_pass http://your-service-__VERSION__:8080;
        ...
    }

  }
}

5. nginx 환경변수 적용을 위한 shell 스크립트 작성

docker-compose.yml에서 nginx를 실행할 때 command항목에서 지정한 쉘 스크립트를 실행한다.

docker-compose.yml

  nginx:
    image: nginx:latest
    restart: unless-stopped
    env_file:
      - ./back.env
    volumes:
      - ./conf/nginx.conf:/etc/nginx/nginx.conf
      - ./start-nginx.sh:/usr/local/bin/start-nginx.sh
      - ./data/certbot/conf:/etc/letsencrypt
      - ./data/certbot/www:/var/www/certbot
      - ./build:/usr/share/nginx/html
      - /usr/share/zoneinfo/Asia/Seoul:/etc/localtime:ro
    ports:
      - "80:80"
      - "443:443"
    command: ["/usr/local/bin/start-nginx.sh"]
    depends_on:
      - app
      - postgresql
    networks:
      - app-network

 

아래의 shell 스크립트를 통해서 환경변수값을 확인 후에 nginx.conf 파일에 적용하여 원래위치에 nginx.conf 파일과 바꿔치기해서 적용시킨다.

#!/bin/bash
# 환경변수 값 확인
echo "Applying VERSION: $VERSION to nginx.conf"

# nginx.conf 파일을 임시 위치로 복사
cp /etc/nginx/nginx.conf /tmp/nginx.conf

# 환경 변수 VERSION을 /tmp/nginx.conf에 적용
sed -i "s/__VERSION__/${VERSION}/g" /tmp/nginx.conf

# Certbot의 웹 루트 설정 추가
if ! grep -q "location /.well-known/acme-challenge/" /tmp/nginx.conf; then
  sed -i "/server_name/a \ \n\
    location /.well-known/acme-challenge/ {\n\
        root /var/www/certbot;\n\
    }\n" /tmp/nginx.conf
fi

# 수정된 /tmp/nginx.conf 파일을 원래 위치로 복사
cp /tmp/nginx.conf /etc/nginx/nginx.conf

# 수정된 nginx.conf 내용 확인 (디버깅)
echo "Modified nginx.conf:"
cat /etc/nginx/nginx.conf

# nginx 구성 검증
nginx -t

# nginx 실행
if [ $? -eq 0 ]; then
    echo "Starting nginx..."
    nginx -g 'daemon off;'
    else
    echo "Configuration test failed"
    exit 1
fi

'배운내용 정리' 카테고리의 다른 글

인프라 분산 구현  (0) 2024.05.01
Nginx + Certbot을 통한 Https 및 TimeZone 적용  (0) 2024.04.15
Github Action CI/CD + Docker  (0) 2024.04.02
JDBC  (0) 2023.11.13
Spring IoC와 DI  (0) 2023.11.10